03 - Code Generation
📋 Jump to TakeawaysYou don't write protobuf serialization code by hand. You write .proto files and the compiler generates Go structs, methods, and gRPC stubs for you. This lesson covers how that pipeline works.
Installing protoc Plugins
protoc is the Protocol Buffers compiler. It doesn't know about Go on its own — it uses plugins. You need two:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestThese install to $HOME/go/bin. Make sure that's in your PATH:
export PATH="$PATH:$(go env GOPATH)/bin"Verify both are available:
which protoc-gen-go # should print a path
which protoc-gen-go-grpc # should print a pathWhat each plugin does:
protoc-gen-go→ generates message types (structs, marshal/unmarshal) → outputs*.pb.goprotoc-gen-go-grpc→ generates gRPC service stubs (server/client interfaces) → outputs*_grpc.pb.go
What's a "service"? It's a block in your .proto file that defines the RPC methods your server will expose — the API contract. We'll cover services in depth in lesson 06, but we need one here to see the full code generation pipeline. For now, just know that service declares functions that clients can call remotely, and each function takes a request message and returns a response message.
If your .proto file has no service definition, protoc-gen-go-grpc has nothing to generate and you'll only get *.pb.go.
Generated File Layout
Here's the structure we'll use throughout this course:
shortener/
├── go.mod
├── main.go
├── proto/
│ └── link.proto ← you write this
└── pb/
├── link.pb.go ← generated (messages)
└── link_grpc.pb.go ← generated (gRPC stubs)Source .proto files live in proto/. Generated Go code goes into pb/. This keeps things separate and makes it obvious which files are hand-written vs machine-generated.
Writing the Proto File
Create proto/link.proto:
syntax = "proto3";
package shortener;
option go_package = "shortener/pb";
message Link {
int64 id = 1;
string url = 2;
string short_code = 3;
int64 clicks = 4;
}
message CreateLinkRequest {
string url = 1;
}
message CreateLinkResponse {
Link link = 1;
}
message GetLinkRequest {
string short_code = 1;
}
message GetLinkResponse {
Link link = 1;
}
service LinkService {
rpc CreateLink(CreateLinkRequest) returns (CreateLinkResponse);
rpc GetLink(GetLinkRequest) returns (GetLinkResponse);
}The service block is what triggers link_grpc.pb.go generation. Without it, you only get link.pb.go.
Running protoc
Create the output directory first — protoc won't create it for you:
mkdir -p pbThen generate:
protoc \
--proto_path=proto \
--go_out=pb \
--go_opt=paths=source_relative \
--go-grpc_out=pb \
--go-grpc_opt=paths=source_relative \
proto/link.protoBreaking this down:
| Flag | What it does |
|---|---|
--proto_path=proto |
Tells protoc where to find .proto files. Without this, the output mirrors the input path and you get pb/proto/link.pb.go (wrong). |
--go_out=pb |
Output directory for message code |
--go_opt=paths=source_relative |
Place files relative to the .proto source, not the go_package path |
--go-grpc_out=pb |
Output directory for gRPC stubs |
--go-grpc_opt=paths=source_relative |
Same as above, for gRPC files |
The double = in --go_opt=paths=source_relative isn't a typo. The first = separates the flag from its value. The second = is part of the value itself — it sets the option paths to source_relative.
Why do we need both --go-grpc_out=pb and paths=source_relative? They do different things. --go-grpc_out=pb sets the root output directory. But without paths=source_relative, protoc uses the go_package value from your .proto file to build subdirectories inside that root. So you'd get pb/shortener/pb/link_grpc.pb.go — deeply nested and wrong. paths=source_relative tells protoc to skip that and just place files flat in the output directory.
After running this, you should have:
pb/
├── link.pb.go
└── link_grpc.pb.goIf you only see link.pb.go, check that your .proto file has a service definition.
Common Mistakes
"No such file or directory" — The output directory (pb/) doesn't exist. Run mkdir -p pb first.
Only link.pb.go generated, no link_grpc.pb.go — Either:
- Your
.protofile has noserviceblock, or protoc-gen-go-grpcisn't installed / not in yourPATH
Files end up in pb/proto/ (nested) — You're missing --proto_path=proto. Without it, protoc preserves the input file's directory structure in the output.
Generated Message Code
The generated link.pb.go contains Go structs for each message:
type Link struct {
Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"`
ShortCode string `protobuf:"bytes,3,opt,name=short_code,json=shortCode,proto3" json:"short_code,omitempty"`
Clicks int64 `protobuf:"varint,4,opt,name=clicks,proto3" json:"clicks,omitempty"`
}Each struct gets ProtoReflect(), Reset(), String(), and the marshal/unmarshal methods. You use these structs like any Go struct:
link := &pb.Link{
Id: 1,
Url: "https://example.com",
ShortCode: "abc123",
Clicks: 42,
}
fmt.Println(link.GetUrl()) // "https://example.com"Notice the getter methods (GetUrl(), GetClicks()). They're nil-safe. If the message is nil, getters return zero values instead of panicking.
The gRPC Stubs
The generated link_grpc.pb.go contains two things:
- A server interface you implement:
type LinkServiceServer interface {
CreateLink(context.Context, *CreateLinkRequest) (*CreateLinkResponse, error)
GetLink(context.Context, *GetLinkRequest) (*GetLinkResponse, error)
mustEmbedUnimplementedLinkServiceServer()
}- A client you use to call the service:
type LinkServiceClient interface {
CreateLink(ctx context.Context, in *CreateLinkRequest, opts ...grpc.CallOption) (*CreateLinkResponse, error)
GetLink(ctx context.Context, in *GetLinkRequest, opts ...grpc.CallOption) (*GetLinkResponse, error)
}We'll implement both in later lessons.
Automate with a Makefile
Running protoc by hand gets old fast. Add a Makefile:
.PHONY: proto
proto:
mkdir -p pb
protoc \
--proto_path=proto \
--go_out=pb \
--go_opt=paths=source_relative \
--go-grpc_out=pb \
--go-grpc_opt=paths=source_relative \
proto/*.protoNow just run make proto whenever you change a .proto file.
Go Module Dependencies
Your go.mod needs the protobuf and gRPC packages:
go get google.golang.org/protobuf
go get google.golang.org/grpc
go mod tidyThe generated code imports these, so you need them before the project compiles. go mod tidy cleans up go.mod and downloads any transitive dependencies.
Don't Edit Generated Files
Never edit *.pb.go files. They get overwritten every time you run protoc. If you need custom methods on a protobuf type, put them in a separate file in the same package.
Key Takeaways
protoccompiles.protofiles into Go code using pluginsprotoc-gen-gogenerates message structs,protoc-gen-go-grpcgenerates service stubs- Always use
--proto_pathto avoid nested output directories protocwon't create output directories —mkdir -pfirst- No
servicein your.proto= no_grpc.pb.gofile - Use
paths=source_relativeto keep generated files predictable - Automate with a Makefile so you never mistype the command
- Never edit generated
*.pb.gofiles