Goten protobuf-go Extension
protobuf-go
Goten extension.Goten builds on top of protobuf, but the basic library for proto is not provided by us of course. One popular protobuf library for Golang can be found here: https://github.com/protocolbuffers/protobuf-go. It provides lots of utilities around protobuf:
- Parsing proto messages to and from binary format (proto wire).
- Parsing proto messages to and from JSON format (for being human-friendly)
- Copying, merging, comparing
- Access to proto option annotations
Parsing messages to binary format and back is especially important, this is how we send/receive messages over the network. However, it does not exactly work for Goten, because of our custom types.
If we have:
message SomeResource {
string name = 1 [(goten.annotations.type).name.resource = "SomeResource" ];
}
The native protobuf-go
library would map the “name” field into the Go
“string” type. But in Goten, we interpret this as a pointer to the Name
struct in the package relevant to the resource. Reference, Filter, OrderBy,
Cursor, FieldMask, and ParentName are all other custom types. Problematic
are strings, how to map them to non-strings if they have special annotations.
For this reason, we developed a fork called goten-protobuf: https://github.com/cloudwan/goten-protobuf.
The most important bit is the ProtoStringer interface defined in this file: https://github.com/cloudwan/goten-protobuf/blob/main/reflect/protoreflect/value.go.
This is the key difference between our fork and the official implementation. It’s worth to mention more or less how it works.
Look at any gengo-generated file, like
https://github.com/cloudwan/goten/blob/main/meta-service/resources/v1/service/service.pb.go.
If you scroll somewhere to the bottom, to the init()
function, are
registering all types we generated in this file. We also pass raw
descriptors. This is how we are passing information to the protobuf
library. It then populates its registry with all proto descriptors
and matches (via reflection) protobuf declarations with our Golang
struct definitions. If it detects that some field is a string in protobuf,
but it’s a struct in implementation, it will try to match with
ProtoStringer
, which should work, as long as the interface matches.
We tried to make minimal changes in our fork, but unfortunately, we sometimes need to sync from the main one.
Just by the way, we can use the following protobuf functions (interface
proto.Message
is implemented by ALL Go structs implemented on protobuf
message type), using google.golang.org/protobuf/proto
import:
-
proto.Size(proto.Message)
To detect the size of the message in binary format (proto wire)
-
proto.Marshal(proto.Message)
Serialize to the binary format.
-
proto.Unmarshal(in []byte, out proto.Message)
De-serialize from binary format.
-
proto.Merge(dst, src proto.Message)
It merges
src
intodst
. -
proto.Clone(proto.Message)
It makes a deep copy.
-
proto.Equal(a, b proto.Message)
It ompares messages.
More interestingly, we can extract annotations in Golang with this library, like:
import (
resourceann "github.com/cloudwan/goten/annotations/resource"
"github.com/cloudwan/goten/runtime/resource"
)
func IsRegionalResource(res resource.Resource) bool {
msgOpts := res.ProtoReflect().Descriptor().
Options().(*descriptorpb.MessageOptions)
resSpec := proto.GetExtension(msgOpts, resourceann.E_Resource).
(*resourceann.ResourceSpec)
// ... Now we have instance of ResourceSpec -> See goten/annotations/resource.proto file!
}
The function ProtoReflect()
is often used to reach out for object
descriptors. It sometimes gives some nice alternative to regular reflection
in Go (but has some corner cases where it breaks on our types… and we
fall back to reflect).