How do you keep protobuf messages backward and forward compatible? Why must you never reuse field numbers?
Question 569MediumGo 1.22 to 1.25
The protobuf wire format identifies each field only by its field number and wire type, never by its name. Two rules follow from this:
- Backward compatibility (new code reads old data): a missing field decodes as its zero value.
- Forward compatibility (old code reads new data): unknown fields are skipped, and in Go they are preserved and re-serialized.
Reusing a number breaks this. An old client, or an old message sitting in a queue or database, will decode the bytes as the old field, perhaps with a different type. You get silent data corruption or parse errors, not a compile error.
message User {
reserved 3, 7 to 9; // numbers of deleted fields: never reuse
reserved "email_legacy"; // stops the old name being reused (JSON/text formats)
string id = 1;
string name = 2;
optional int32 age = 4; // explicit presence: tells "unset" apart from 0
Status status = 5;
repeated string tags = 6;
}
enum Status {
STATUS_UNSPECIFIED = 0; // the zero value must mean "unknown"
STATUS_ACTIVE = 1;
}
Safe changes: add new fields with new numbers, delete a field and reserve its number and name, rename a field (binary-safe but breaks JSON), and add enum values when readers handle unknown values.
Unsafe changes:
- Changing a field's number or type. There are a few wire-compatible exceptions, such as
int32/int64/uint32/uint64/bool, but values can be truncated. - Moving existing fields into or out of a
oneof. - Changing a singular field to
repeatedfor message types. - Making a field
required(proto2).
Gotchas:
- With proto3 scalars, 0 and "not set" look the same unless you declare the field
optional. - Compare messages with
proto.Equal, not==orreflect.DeepEqual. - Never copy generated structs by value. They embed a no-copy guard that
go vetflags. - Enforce these rules in CI with
buf breaking.
More on Observability, Debugging & Production Operations
- Q567How do you add distributed tracing with OpenTelemetry in Go? How does the trace context flow through context.Context and HTTP/gRPC?
- Q568Explain gRPC in Go: unary vs streaming RPCs, interceptors, deadlines, status codes and error details.
- Q570How do you implement health checks (liveness vs readiness) for a Go service running in Kubernetes, and how do they interact with graceful shutdown?
- Q571How do you build a minimal, secure Docker image for a Go service (multi-stage build, scratch/distroless, CGO_ENABLED=0, CA certs, time zone data)?
- Q572How do you manage configuration in Go (env vars, files, flags)? How do you validate it and reload it safely at run time?
- Q573How do you configure TLS correctly in Go (crypto/tls MinVersion, certificate reloading, mTLS)?