3 ms·
> > - Messages (especially opaque ones) are not supposed to be copied. > So? If you have a []mypb.Message, and range over it in the normal way: if _, m :=
by aktau 2y ago
> > - Messages (especially opaque ones) are not supposed to be copied.
> So?
If you have a []mypb.Message, and range over it in the normal way:
if _, m := range msgs {
// Use.
}
That makes a copy of the struct. This is not supported in general for the opaque API, even though it appears to work for standard use cases. The representation is meant to be opaque.
- cyberax 2y agoYou start with the wrong premise that the messages shouldn't be copied in the first place. Why?
- aktau 2y agoCopying makes for surprising semantics, and prevents some representation changes. An example w.r.t. the surprising semantics: var ms []mypb.Message = ppb.GetMessages() // A repeated submessage field for i, m := range ms { m.SetMyInt(i) } assert(ppb.GetMessages()[1].GetMyInt(1) == 1) // This would fail in general, due to SetMyInt acting on a copy. This would not work as expected, as I highlighted in the comment. Basically, acting on value types means being very careful about identity. It makes it easy to make mistakes. I like data-driven code, but working around this (sometimes you'd want a copy, sometimes you wouldn't) would be a painful excercise. You may have noticed that changing a heavily pointerized tree of types into value types often compiles with just a few changes, because Go automatically dereferences when needed. But it often won't work from a semantic point of view because the most intuitive way to modify such types uses copies (the range loop is a good example). Now imagine changing the representation such that it carries a mutex, or another nocopy type. That would lead to issues unless those nocopy types would be encapsulated in a pointer. But then you get issues with initialization: var m mypb.Message // Value type, but what about the *sync.Mutex contained deep within? Also consider laziness func process(lm mypb.LazyMessage) { if lm.GetSubMessage().GetInt() != 42 { panic("user is not enlightened") } } var lm mypb.LazyMessage process(lm) // Copy a fairly large struct. ln.GetSubMessage().GetInt() // Does lazy unmarshaling of sub_message redundantly. If you want to make the argument that individual messages should be pointers, but slices should still be value slices. Then I have the following for you: ms := m.GetSubMessages() // []mypb.Message el := &ms[0] anotherEl := new(mypb.Message) ms.SetSubMessages(append(ms.GetSubMessages(), anotherEl)) // Can cause reallocation, now el no longer references to m.GetSubMessages()[0]. But it no reallocation happened, it does. In practice, value typing leads to a bunch of issues. Since you seem so sure of your position, I'm actually curious. How would you design the API, how would you use it? Do you have any examples I can look at of this style being used in practice?