Generating Go Code

Turn a .proto file into Go with protoc and its two plugins, and keep the generated files in the repository.

Syntaxprotoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative api/user/v1/user.proto

You do not write the Go types for a protobuf message by hand. A compiler writes them for you. That compiler is protoc, and on its own it knows nothing about Go: it parses .proto files and hands the result to a plugin, which is a separate program that emits code for one language.

The two plugins

Go needs two. protoc-gen-go writes user.pb.go, holding the structs and their getters. protoc-gen-go-grpc writes user_grpc.pb.go, holding the client stub (a Go type whose methods make network calls) and the server interface you implement. You ask for them with --go_out and --go-grpc_out. protoc locates a plugin by name alone: --go_out looks for a program called protoc-gen-go on your PATH.

Where the files land

Add paths=source_relative to each plugin and every generated file is written next to the .proto file it came from. Without it, protoc rebuilds the directory tree from the go_package import path, which is almost never what you want in a single module.

Commit the output

Check the generated files into version control. A fresh clone then builds with go build and no protoc installed, review can see exactly what changed when a message changed, and continuous integration needs no extra toolchain. Record the command in a //go:generate comment, which is the short way of writing the long protoc line once and then typing go generate ./.... Since Go 1.24 you can also pin the plugins in go.mod with go get -tool and run them as go tool protoc-gen-go, rather than trusting whatever an earlier go install left in your PATH.

Example

Example · go
package main

import (
	"fmt"

	"google.golang.org/protobuf/types/known/wrapperspb"
)

func main() {
	// Generated code carries a descriptor: the .proto definition, compiled in.
	// Int32Value was generated from wrappers.proto by protoc-gen-go, exactly
	// the way your own messages are.
	md := (&wrapperspb.Int32Value{}).ProtoReflect().Descriptor()
	fmt.Println("message:", md.FullName())

	fields := md.Fields()
	for i := range fields.Len() { // range over an int: Go 1.22 and later
		f := fields.Get(i)
		fmt.Printf("field %d: %s (%s)\n", f.Number(), f.Name(), f.Kind())
	}
}

// Output:
// message: google.protobuf.Int32Value
// field 1: value (int32)

When to use it

  • Regenerate Go types after adding a field to a message, in one command the whole team runs the same way.
  • Pin plugin versions in go.mod so two developers generate byte-identical files.
  • Add a CI check that fails when a committed .pb.go file no longer matches its .proto file.
  • Generate only the message types, with no gRPC code, for a service that sends protobuf over a queue.

More examples

Installing the plugins and running protoc

The repeatable version. `go get -tool` writes a tool directive into go.mod, which pins the plugin versions alongside your library versions; `go install tool` builds every pinned tool into one directory. Generated code changes when a plugin version changes, so pinning is what stops a teammate's regeneration from producing a large unrelated diff. protoc itself is a C++ program and is installed separately, by your package manager.

Example · bash
# Record both plugins in go.mod (Go 1.24 and later) so every machine
# and CI job uses the same versions.
go get -tool google.golang.org/protobuf/cmd/protoc-gen-go@latest
go get -tool google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

# protoc needs the plugins as programs on PATH, so put them in ./bin.
GOBIN=$PWD/bin go install tool

PATH=$PWD/bin:$PATH protoc \
	--go_out=. --go_opt=paths=source_relative \
	--go-grpc_out=. --go-grpc_opt=paths=source_relative \
	api/user/v1/user.proto

Recording the command with go:generate

A `//go:generate` comment is a line of shell that `go generate` runs when it finds it. It is the shorthand for the full protoc invocation, and the two are the same command. Keep one in the package that owns the .proto files so nobody has to remember the flags, and add a CI step that regenerates and fails if the working tree changed, which catches a committed file that no longer matches its .proto.

Example · go
// Package api holds the generated protobuf and gRPC code for this service.
//
// After editing any .proto file, run: go generate ./...
package api

//go:generate protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative user/v1/user.proto

What protoc-gen-go-grpc actually writes

Worth reading once so the generated file stops being a black box. The last method on the server interface is unexported, which means no package but the generated one can write it; that is deliberate, and it forces you to embed UnimplementedUserServiceServer. The payoff is that adding an rpc to the .proto file never breaks the build of an existing server. Reach for this listing when a compile error says your type does not implement the server interface.

Example · go
// user_grpc.pb.go, written by protoc-gen-go-grpc. Do not edit.

// The client stub: one method per rpc, each one a network call.
type UserServiceClient interface {
	GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error)
}

// The server side: the interface your own type must satisfy.
type UserServiceServer interface {
	GetUser(context.Context, *GetUserRequest) (*User, error)
	mustEmbedUnimplementedUserServiceServer()
}

// Embedding this struct supplies that unexported method, and a default
// implementation of every rpc you have not written yet.
type UnimplementedUserServiceServer struct{}

Discussion

  • Be the first to comment on this lesson.