Great, now there's an API per struct/message to learn and communicate throughout the codebase, with all the getters and setters.
A given struct is probably faster for protobuf parsing in the new layout, but the complexity of the code probably increases, and I can see this complexity easily negating these gains.
> Great, now there's an API per struct/message to learn and communicate throughout the codebase, with all the getters and setters.
No, the general idea (and practical experience, at least for projects within Google) is that a codebase migrates completely from one API level to another. Only larger code bases will have to deal with different API levels. Even in such cases, your policy can remain “always use the Open API” unless you are interested in picking up the performance gains of the Opaque API.
message M {
string foo = 1;
}
message N {
M bar = 2;
}
I find (new(M)).Bar.Foo panicking pretty annoying. So I just made it a habit to m.GetBar().GetFoo() anyway. If m.GetBar().SetFoo() works with the new API, that would be an improvement.
There are some options like nilaway if you want static analysis to prevent you from writing this sort of code, but it's difficult to retrofit into an existing codebase that plays a little too fast and loose with nil values. Having code authors and code reviewers do the work is simpler, though probably less accurate.
The generated code's API has never really bothered me. It is flexible enough to be clever. I especially liked using proto3 for data types and then storing them in a kv store with an API like:
type WithID interface { GetId() []byte }
func Put(tx *Tx, x WithID) error { ... }
func Get(tx *Tx, id []byte) (WithId, error) { ... }
The autogenerated API is flexible enough for this sort of shenanigan, though it's not something I would recommend except to have fun.
I found that this ends up being a giant amount of useless code, and a ton of memory allocation noise, that only satisfied my desire for elegance. I've given up that approach and just use protobuf types throughout as the base type. I got sick of writing dumb conversion funcs.
It’s fairly mindless boilerplate for sure, but it does mean that when something happens that causes a change like this protobuf update, the change in your codebase is isolated just to the interface between it and your code ie your dumb conversion funcs. Otherwise you end up with the problem the original commenter had.
It’s good to isolate your dependencies within the code :)
At which point I loose all the benefits of lazy decoding that the accessor methods can provide, so I could just decode directly into a sensible struct, except you can’t with Protobuf.
Well it depends. If your data model doesn't include "this bool is optional", you can just include the bool directly in the struct and get all the memory layout advantages, and then you decide in your protobuf -> domain type conversion code whether it's an error if that field is missing or if it just defaults to 'false'. You only need to make ways for a field to be optional (such as naming it a pointer where nil represents "missing") when that actually makes sense in your data model.
I've done this, it only makes sense to me if you're trying to recycle some legacy code that's already using the domain types. Or else there's a bunch of extra conversion logic and unnecessary copying, feels like an antipattern
I mean calling it "a new API per message" is a bit of an exaggeration... the "API" per message is still the same: something with some set of attributes. It's just that those attributes are now set and accessed with getters and setters (with predictable names) rather than as struct fields. Once you know how to access fields on protobuf types in general, all message-specific info you need is which fields exist and what their types are, which was the case before too.
A given struct is probably faster for protobuf parsing in the new layout, but the complexity of the code probably increases, and I can see this complexity easily negating these gains.