Lesson 2 · Foundations
Protobuf: messages & fields
The contract — and why the field number is sacred.
Your win: read and write a Protobuf message, and explain why you can
rename a field freely but must never reuse or renumber one — the rule buf's
breaking-check exists to protect.
A message is a typed, numbered record
Protobuf messages are declared in a .proto file. Each field has a
type, a name, and — crucially — a number.1
syntax = "proto3";
package spike.v1;
option go_package = "…/spike/v1;spb"; // → Go alias spb, in pkg/manabuf
message SendEmailPayload {
message Content { string PlainText = 1; string HTML = 2; } // nested
string subject = 1; // scalar
Content content = 2; // composite (another message)
repeated string recipients = 3; // repeated = a list
EmailType type = 7; // enum
}
enum EmailType { EMAIL_TYPE_UNSPECIFIED = 0; TRANSACTIONAL = 1; } // first MUST be 0
This compiles to a Go struct (in pkg/manabuf/spike/v1, alias
spb) with fields Subject, Content,
Recipients []string, etc.
The field number is the wire identity
Field-number facts worth knowing: they range 1–536,870,911; numbers 19,000–19,999 are reserved; and 1–15 encode in a single byte, so hot fields get low numbers. In proto3, every field is optional-ish (has a zero value) — echoing Go's zero values you learned in the Go course.
make lint-proto exists
Our repo runs buf purely to lint protos and detect breaking
changes (proto/buf.yaml, make lint-proto) — precisely to
stop someone reusing a field number and corrupting the contract. (Codegen is a separate
step — Lesson 3.) Enum first-value *_UNSPECIFIED = 0 is enforced too.
reserved 4;) so no
one accidentally reuses it later. Removing a field without reserving its number is how
wire-compatibility bugs sneak in.
Protocol Buffers — Language Guide (proto3)
The definitive guide to messages, scalar types, repeated, enums, and
the field-number rules. Read the "Assigning Field Numbers" section closely.
Check yourself (from memory)
Q1. In a proto message, what identifies a field on the wire?
Q2. Changing a field's name (keeping its number) is…
Q3. In proto3, an enum's first value must be…
*_UNSPECIFIED — it's the
field's zero value when unset.
buf's breaking-change check (make lint-proto)
guards — and why you reserved a deleted field's number.oneof, map fields, or how proto3 handles
optional/presence? Ask me.