Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Go newbie here. Can anybody elaborate on this piece of the docs?

""" You might be thinking that it would be more usual to have an ops.Add(ColorOp{Color: red}) method instead of using op.ColorOp{Color: red}.Add(ops). It’s like this so that the Add method doesn’t have to take an interface-typed argument, which would often require an allocation to call. This is a key aspect of Gio’s “zero allocation” design. """

Why would there be an allocation? Of what? How are we saving it?



This has to do with how Go handles dynamic typing via interfaces and how structs fit into that.

When a function takes an interface type as an argument, and you pass a pure struct to it, it creates a wrapper around it (this is the allocation referred to in your quote) which is simply a pair of pointers, one being a type/vtable pointer and the other being a pointer to the struct's data.

Doing it this way allows code to do run-time type inference as well as allow interface extension implicitly (that is, implementing an interface is done by implementing its methods, and you don't have to explicitly type it like ByteReader extends Reader"). Since you only pay this cost if you use it, a lot of fast-path code will use structs exclusively if it can.


How do generics change this? Can a type bound on a generic work like a static interface implementation specification, and recover the cost of wrapping the struct?


You can declare generic structs and generic functions, and get monomorphized copies (not erased to "any") of each function for all concrete types used. You can't look them up by reflection. You can't declare generic methods, and still can't overload methods by type.


> get monomorphized copies (not erased to "any") of each function for all concrete types used

That's not exactly true. You get one concrete function for all generic pointer types for instance. Generic functions take a hidden paramater called a dictionnary, which is fairly similar to vtable, since it contains type information and pointers for all methods of the generic type that the function calls.

So methods on generic types are still performed through an indirection.

https://github.com/golang/proposal/blob/master/design/generi...


Thanks, had not seen that before. It's odd that they came close to reimplementing method dispatch for builtin types, but didn't surface it for developers.


That'd be how C#'s generics work for struct Ts. Go uses GC shape sharing: https://deepsource.com/blog/go-1-18-generics-implementation


Whenever you do:

  v := interfaceType(concreteTypeValue)
in Go, what you're actually doing on a lower level is:

  dataPtr := &concreteTypeValue
  typePtr := typeData[concreteType]()
  v := interfaceData{
          data: dataPtr,
          typ: typePtr,
  }
The first line here is the allocation, since (at least, the way I recall the rule) in Go pointers never point to values on the stack, so concreteTypeValue must be allocated on the heap. The rule about pointers not pointing to the stack is there to make it easier for goroutine stacks to grow dynamically.

See https://go.dev/doc/faq#stack_or_heap.


>in Go pointers never point to values on the stack

This is only the case for pointers that the compiler can't prove not to escape:

>In the current compilers, if a variable has its address taken, that variable is a candidate for allocation on the heap. However, a basic escape analysis recognizes some cases when such variables will not live past the return from the function and can reside on the stack.


That could be true, but I've done a few optimizations of allocations in Go code, and I don't recall pointers to stack values ever being optimized (unless the entire branch is removed, of course). If anyone could provide an example of the code that does pointer operations yet doesn't cause allocations, I'd appreciate it!



Ah, so extremely localized uses of pointers, got it, thanks.


More or less, yes, but it doesn't have to be extremely local. Here's a variant of the example that introduces a function call boundary:

https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename...


Oops, copy paste error there. Here is the example with the function call boundary:

https://godbolt.org/#g:!((g:!((g:!((h:codeEditor,i:(filename...


On the first approach ColorOp{Color: red} would have to be boxed and thus heap allocated. This is because ops.Add in general would receive a (fat) pointer to a value implementing a particular interface rather than a value of concrete type.

(The sibling comment written at the same time has a more detailed explanation of the 'fat' part.)


> You might be thinking that it would be more usual to have an ops.Add(ColorOp{Color: red}) method instead of using op.ColorOp{Color: red}.Add(ops)

Others answered your question, but on a different subject: I find that weird to read. To me

  op.ColorOp{Color: red}.Add(ops)
reads like “add ops to the result of op.ColorOp{Color: red}. I would name that function AddTo:

  op.ColorOp{Color: red}.AddTo(ops)
Still unconventional, but at least it messages that the function argument gets modified.



Thanks all (:


Heh. Go just doesn’t let you write zero-cost generic abstractions the way C# does.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: