I’ve had my fair share of dealing with proto files in go (and to some extent JS), so I thought I’d share some stuff I’ve learnt the hard way by working with proto files.
Protoc include paths
The protoc
include paths can be pretty confusing, so I’ll give a few examples
of how to use it properly.
Just include the current directory
protoc
requires that the files referenced are in the include path, so if you’re
referencing files relative to the current directory, you’ll need to specify -I.
,
which also means the protoc
will resolve
all proto import paths relative to the current directory.
$ protoc myproto/myproto.proto -I. --go_out=:.
As long as your proto file imports are all relative to the current directory, this will work.
Several include paths
If you’re using the grpc-gateway
you’ll have to include the google/api/annotations.proto
proto file.
The way I’ve always done that is by vendoring the proto files and adding the vendor path
as an include path:
$ protoc myproto/myproto.proto -I. -I./vendor/github.com/googleapis/googleapis/ --go_out=:.
Includes are specified in order of priority, so protoc
will first see if
./google/api/annotations.proto
exists, and if it doesn’t, it’ll check
./vendor/github.com/googleapis/googleapis/google/api/annotations.proto
.
Note that vendoring is obviously a Go thing, so this might not chime well
with the other Devs in the office who want to keep the proto repository language agnostic.
In that case, you’ll probably define some third_party
folder where you can put
the external dependencies.
Use the go_package
option
This isn’t something readily advertised in the introduction to protobuffers in Go or Go gRPC quick start, but I find it is essential if you ever want to import proto definitions from one proto file to another.
Raison d’être
For example, lets say we have person.proto
in person/person.proto
. It defines the
proto package person
and the message Person
.
syntax = "proto3";
package person;
message Person {
string Name = 1;
uint32 Age = 2;
}
We also have team.proto
in team/team.proto
, defining the proto package
team
and the message Team
. A Team
consists of a sorted list of Person
s,
so team.proto
will need to import that definition from person.proto
.
No problem, just add an import person/person.proto
to team.proto
and reference it
using the namespace specified by the person.proto
package name:
syntax = "proto3";
package team;
import "person/person.proto";
message Team {
repeated person.Person people = 1;
}
When we generate a go file from this definition using protoc
, we’ll end up with a Go file
that imports person/person.pb.go
. That’s no good!
Enter the go_package
option.
Using the go_package
option
For a proto file defined in github.com/myuser/myprotos/myproto/myproto.proto
the
appropriate go_option
value would be github.com/myuser/myprotos/myproto
.
This means that the protoc
compiler can generate a go file that will include the package
github.com/myuser/myprotos/myproto
if you have another proto file that depends
on myproto/myproto.proto
.
So let’s fix the person.proto
and team.proto
proto files.
syntax = "proto3";
option go_package = "github.com/myuser/myprotos/person";
package person;
message Person {
string Name = 1;
uint32 Age = 2;
}
syntax = "proto3";
option go_package = "github.com/myuser/myprotos/team";
package team;
import "person/person.proto";
message Team {
repeated person.Person members = 1;
}
Now, you might say we don’t strictly need to specify the go_package
for team.proto
,
since nothing imports it at the minute. I’d still suggest adding it to all the proto files
that you’ll generate go code from so that in the future when a dependency might arise, you’ve
saved yourself, or even better, someone else, a whole lot of head scratching.
protoc-gen-go
output paths with go_package
option
One final note on the go_package
option. Specifying it in your proto file means the
protoc-gen-go
protoc
plugin outputs your generated files as if the specified output directory is at the root of the go_package
path. So… you’ll probably want to slightly modify your protoc
line:
protoc person/person.proto team/team.proto -I. --go_out=:$GOPATH/src
This should mean the files appear where you expect them to appear. Mind you make sure there
are no typos in the go_package
option as it means the files will be generated in wrong place.
protoc
plugin parameters
Another thing I’ve learned through hours staring at my terminal in bewilderment is
how parameters are passed to protoc
plugins. For example, the protoc-gen-go
plugin
allows you to specify plugins=grpc
as a parameter, and the protoc-gen-grpc-gateway
takes
a boolean parameter logtostderr=true
. I also think the M
parameter is a protoc
-wide way
to change the import path of a specific import as defined in a proto file. Parameters are
comma (,
) separated. Parameter specification is delimited by the colon (:
) character,
after which comes the desired output path.
The following are all valid protoc
commands illustrating this:
$ protoc myproto/myproto.proto -I. --go_out=plugins=grpc:.
$ protoc myproto/myproto.proto -I. --grpc-gateway_out=logtostderr=true,Mgoogle/api/annotations.proto=myrepo/api/annotations.proto:.
More
Feel free to reach out to me on gophers slack or on my twitter if you found this helpful or if you have any more tips I should include in this list.