03 - Code Generation

📋 Jump to Takeaways

You 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@latest

These 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 path

What each plugin does:

  • protoc-gen-go → generates message types (structs, marshal/unmarshal) → outputs *.pb.go
  • protoc-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 pb

Then 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.proto

Breaking 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.go

If 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 .proto file has no service block, or
  • protoc-gen-go-grpc isn't installed / not in your PATH

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:

  1. A server interface you implement:
type LinkServiceServer interface {
    CreateLink(context.Context, *CreateLinkRequest) (*CreateLinkResponse, error)
    GetLink(context.Context, *GetLinkRequest) (*GetLinkResponse, error)
    mustEmbedUnimplementedLinkServiceServer()
}
  1. 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/*.proto

Now 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 tidy

The 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

  • protoc compiles .proto files into Go code using plugins
  • protoc-gen-go generates message structs, protoc-gen-go-grpc generates service stubs
  • Always use --proto_path to avoid nested output directories
  • protoc won't create output directories — mkdir -p first
  • No service in your .proto = no _grpc.pb.go file
  • Use paths=source_relative to keep generated files predictable
  • Automate with a Makefile so you never mistype the command
  • Never edit generated *.pb.go files

💻 Examples

Complete examples for this lesson. Copy and run locally.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue

© 2026 ByteLearn.dev. Free courses for developers. · Privacy