protocolbuffers/protobuf-go
> The canonical Go implementation of Protocol Buffers — the protoc-gen-go code generator plus the google.golang.org/protobuf runtime.
GitHub repo · Official website · License: BSD-3-Clause
Overview
protobuf-go is the second-generation Go API for Protocol Buffers, published under the module path google.golang.org/protobuf and released in 20201. It supersedes the original github.com/golang/protobuf (colloquially APIv1), which dates back to 2010 and predates Go 1. The two are not competitors: as of golang/protobuf v1.4.0, the old module is a thin shim that wraps this runtime, so almost every Go program that touches protobufs eventually depends on this repository1.
The project ships two coupled artifacts: protoc-gen-go, a plugin to Google's protoc/buf compiler that turns .proto schemas into Go structs, and a runtime module of packages (proto, protojson, prototext, protowire, protoreflect, and generated well-known types) that serialize and introspect those structs. The defining design decision of APIv2 is protobuf reflection: the proto.Message interface was reduced to a single ProtoReflect() protoreflect.Message method, moving the API from "an interface that marks a struct" to "an interface that describes message behavior"2. This is what makes dynamic messages, dynamicpb, and general-purpose traversal possible without brittle Go-reflection assumptions about field layout.
The tension is stability-versus-ergonomics. The maintainers (the Go team at Google) treat wire and API compatibility as near-sacred and version conservatively, but the generated Go is verbose, the module split confuses newcomers, and gRPC code generation lives in a separate plugin. This is infrastructure software, optimized for correctness and 10-year horizons over developer delight.
Getting Started
# Install the generator and (separately) the gRPC generator
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# Generate Go from a .proto (protoc must be on PATH)
protoc --go_out=. --go_opt=paths=source_relative user.proto
package main
import (
"fmt"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/encoding/protojson"
userpb "example.com/gen/user"
)
func main() {
u := &userpb.User{Id: 42, Name: "Ada"}
wire, err := proto.Marshal(u) // binary wire format
if err != nil {
panic(err)
}
got := &userpb.User{}
if err := proto.Unmarshal(wire, got); err != nil {
panic(err)
}
// protojson, NOT encoding/json — generated structs lack usable json tags
j, _ := protojson.Marshal(got)
fmt.Println(string(j)) // {"id":"42","name":"Ada"}
}
Architecture / How It Works
The runtime is layered so that the public API never exposes struct internals:
protoreflectdefines the reflective interfaces —Message,Field,FileDescriptor,EnumDescriptor. Everything general-purpose (marshaling, JSON,cmp) is written against these, not against concrete generated types.protoimpl/internal/implis the fast path. Generated messages embed astatefield and a package-level table of field coders built once via reflection, then cached. Hot marshaling does not re-reflect on every call.protoregistryholds a global registry of every file and message descriptor. Generated packages register themselves ininit(). This global state is central toAnyresolution anddynamicpb, and is also the source of the project's most notorious runtime panic (see Production Notes).- Generated code is intentionally coupled to a runtime of a compatible version.
protoimpl.EnforceVersionconstants are compiled in so that a too-old runtime paired with too-new generated code fails at build time rather than mysteriously at runtime. The promise: a runtime stays compatible with generated code produced by a generator no more than one year older3.
Serialization formats are separate packages by design: protowire is the raw varint/tag codec, proto is the binary API most users want, protojson and prototext are the JSON and text projections. protoc-gen-go deliberately does not emit gRPC service stubs — that moved to protoc-gen-go-grpc in 2020, so service and message code are generated by two independently versioned plugins4.
In 2024 the project added the Opaque API: an alternative generated-code shape where message fields are unexported and accessed through getters/setters, enabling lazy decoding, smaller structs, and reduced GC pressure. It is opt-in per file via protoc-gen-go options and coexists with the classic open-struct output5.
Production Notes
The differentiator between "works in a demo" and "works in a large binary" is a handful of well-known footguns:
- Global registry conflicts. If two copies of the same generated
.proto(different module paths, vendored duplicates, or two versions of a shared proto) land in one binary,init()panics at startup withproto: file "X" is already registered. This is a link-time/dependency-hygiene problem, not a code bug, and it has broken many first deploys after a dependency bump. Mitigations: a single source of generated types,protoc-gen-goMimport-path mappings, or theGOLANG_PROTOBUF_REGISTRATION_CONFLICTescape hatch (which downgrades the panic and is a last resort). protojsonoutput is intentionally unstable. The encoder injects nondeterministic whitespace so that callers cannot depend on byte-exact JSON. Do not diff, hash, or snapshot-test rawprotojsonbytes; unmarshal and compare semantically (protocmp.Transformwithcmp).- Binary marshaling is not canonical either.
proto.Marshaldoes not guarantee a stable byte sequence across versions, and map field order is randomized.proto.MarshalOptions{Deterministic: true}sorts map keys but is explicitly not a canonical/signable encoding — never use protobuf bytes as a cryptographic digest input across processes or versions. - Use
protojson/prototext, neverencoding/json. Generated structs do not carry meaningfuljsontags; the standard library will emit wrong field names, mishandleoneof, enums, and well-known types likeTimestamp. - APIv1↔APIv2 bridging. Old code holding
github.com/golang/protobuf/proto.Messageand newgoogle.golang.org/protobufmessages interoperate, but mixing the twoprotopackages in one file invites subtle type mismatches.protoadaptexists specifically to convert between the two message views. - Issues are filed elsewhere. The tracker for this module historically lived at
golang/protobuf, so searching this repo's issues for a bug can come up empty.
When to Use / When Not
Use when:
- You are writing any Go service that speaks gRPC or consumes/produces
.proto-defined messages — this is the reference implementation, not one option among many. - You need protobuf reflection: dynamic messages, schema-driven traversal,
Anyhandling, or building your own codecs againstprotoreflect. - You want wire compatibility guarantees and a maintainer team that treats breaking changes as a six-month-notice event3.
Avoid / look elsewhere when:
- You want a single self-contained codec with no external
protoctoolchain — reach forvtprotobufoutput or a schema-less format. - You need maximum raw throughput and minimal allocation on a hot path — evaluate the Opaque API or
planetscale/vtprotobuf's generated fast marshalers before assuming the default runtime is fast enough. - Your data is small, human-authored config — protobuf's schema/codegen overhead is unjustified versus JSON/TOML.
Alternatives
- gogo/protobuf — the historically faster APIv1-era fork with generated marshalers; now effectively unmaintained and incompatible with APIv2, but still in legacy codebases. Use it only to keep an existing system running, not for new work.
- planetscale/vtprotobuf — a
protoc-gen-gocompanion plugin that emits allocation-freeMarshalVT/UnmarshalVTmethods. Use it with protobuf-go when serialization is a measured bottleneck. - bufbuild/protocompile + bufbuild/protovalidate — use Buf's toolchain when you want a
protoc-free build, lint, breaking-change detection, and runtime validation. - google.golang.org/grpc — the service/RPC layer; pair
protoc-gen-go-grpcwith this repo, since protobuf-go itself generates no service stubs. - google/flatbuffers or capnproto — use these instead when you need zero-copy access and can trade away protobuf's ecosystem.
History
| Version | Date | Notes |
|---|---|---|
| APIv1 | 2010 | github.com/golang/protobuf first released publicly; predates Go 11. |
| v1.20.0 | 2020-03 | First google.golang.org/protobuf release; new reflection-based API12. |
| golang/protobuf v1.4.0 | 2020-03 | Old module rewired as a shim over the new runtime1. |
| — | 2020 | gRPC codegen split out into protoc-gen-go-grpc4. |
| — | 2024 | Protobuf Editions support; Opaque API introduced5. |
References
- ^ Joe Tsai, Damien Neil, Herbie Ong, "A new Go API for Protocol Buffers" — The Go Blog, 2020-03-02. https://go.dev/blog/protobuf-apiv2
- ^ Package
proto/protoreflectreference — theProtoReflect() protoreflect.Messagemessage interface. https://pkg.go.dev/google.golang.org/protobuf/proto#Message - ^ protobuf-go README, "Compatibility" — breaking-change policy and
protoimpl.EnforceVersion. https://github.com/protocolbuffers/protobuf-go#compatibility - ^
protoc-gen-go-grpc(in the grpc-go repo). https://github.com/grpc/grpc-go/tree/master/cmd/protoc-gen-go-grpc - ^ "Go Protobuf: The new Opaque API" — The Go Blog, 2024. https://go.dev/blog/protobuf-opaque
Tags
go, golang, protocol-buffers, protobuf, serialization, code-generation, grpc, wire-format, reflection, rpc