A struct tag reads schema: "user_id" with a space after the colon — why does the Go build still pass?
answer
- the compiler never reads the string
- one whitespace character changes everything
- a vet analyzer knows the grammar
- grammar is checkable, meaning is not
- pin the tags in a test
basics
~20 sA struct tag is an unchecked string literal, so any content compiles. The space breaks the conventional key:"value" grammar, so the key is never found at run time. go vet's structtag check reports malformed tags.
solid answer
~50 sThe compiler requires only that a tag be a valid string literal, so `schema: "user_id"` builds cleanly. The conventional grammar is `key:"value"` with no space after the colon, so at run time the lookup for the `schema` key finds nothing and the reading library falls back to its default — usually the field's own name. Nothing fails; the field just quietly gets the wrong wire name, which typically surfaces days later as a field that never round-trips. Two defences. Run `go vet ./...` in CI: its structtag analyzer knows the grammar and reports malformed tag pairs, duplicate names among known keys, and a tagged field that is unexported. Then, for tags whose values are a contract, write a table-driven test that walks the struct's fields and asserts the exact tag each one carries. `go test` only runs a subset of vet's analyzers, so the explicit vet step earns its place.
code
go · 3 linestype Column struct {
UserID int `schema: "user_id"`
}go deeper
Know that struct tags are plain strings the compiler never inspects, so a typo inside one produces no error at all — only wrong behaviour later, at run time.
Explain exactly which grammar the space after the colon breaks, what the reading library does when its key is absent, and name go vet's structtag analyzer as the tool that reports malformed tags.
Show the layered defence you would actually put in place: go vet ./... as its own CI step, a test asserting each field's tag on contract structs, and treating a tags-only diff as a schema change during review.
Decide where tag correctness is enforced across many services — generating types from one schema source versus hand-written tags plus a shared CI baseline — and name who owns the wire contract when a tag has to change.
## Why the compiler is silent A struct tag is a string literal and nothing more. The compiler checks that it *is* a literal — an unterminated one is a syntax error — and then stores the bytes with the field's type. It has no opinion about the contents, because the format inside the string is a convention among libraries, not part of the language. So every one of these compiles: ```go type Column struct { UserID int `schema: "user_id"` // space after the colon } ``` ```go type Column struct { UserID int `scheme:"user_id"` // key misspelled } ``` ```go type Column struct { UserID int `schema:"user_id", doc:"pk"` // comma between pairs } ``` The conventional reader splits the tag on spaces and expects each piece to be `key:"value"` with the colon immediately followed by an opening quote. The first and third examples fail that grammar, so the `schema` key is simply not found. The second parses perfectly — it just answers a question nobody asks. In every case the failure mode is identical and it is the dangerous one: **the lookup returns nothing and the library applies its default**, usually the Go field name. On a service whose structs define a wire or schema contract, that means a field silently published under `UserID` instead of `user_id`. There is no error, no warning, and no panic. It surfaces when a consumer reports a missing field, or when a round-trip test that nobody wrote would have caught it. ## What go vet catches The toolchain does have a check for this: `go vet` includes a **structtag** analyzer that understands the conventional grammar. It reports: - malformed pairs — a space after the colon, an unquoted value, the wrong separator; - duplicate names among the keys it knows about, so two fields cannot claim the same encoded name; - a field carrying an encoding tag while being unexported, which can never work because outside code cannot set it. So `go vet ./...` in CI turns the first and third examples above into build failures. This is one of the highest-value vet checks precisely because the compiler cannot help. One trap: `go test` runs only a **subset** of vet's analyzers over the package under test. Do not assume a green test run has vetted your tags — make `go vet ./...` its own CI step so every package and every default analyzer runs. ## What go vet cannot catch Vet checks grammar, not meaning. It has no idea what your `schema` key is for. Everything below is well-formed and invisible to it: - the key misspelled (`scheme` for `schema`) — the tag is syntactically perfect; - the right key with the wrong value (`user`, when the registry expects `user_id`); - a tag copy-pasted onto the neighbouring field during a refactor; - a value that was correct until the downstream contract changed. For a struct whose tags *are* the contract, the answer is a test. A table of field name to expected tag value, plus a loop over the struct type's fields comparing each one, converts every future tag edit into a deliberate act: the test fails, the author updates the expectation, and the reviewer sees the contract change as a diff line rather than as an invisible character. ## The review posture The person reviewing the pull request is the last line here, and the useful habit is a reframing: **a tag edit on an exported struct is a schema change, not a cosmetic one.** Concretely: - Treat a diff that touches only tags with the same care as one that renames an exported field. - Ask whether a consumer reads that key, and whether the change needs to ship in a particular order relative to them. - Prefer generating the struct from the single source of truth when one exists — a hand-maintained tag on a generated contract is a typo waiting for a quiet afternoon. - Keep the tagged wire struct separate from the domain type when the two evolve independently. Tags then live in one small file that reviewers know to read carefully, rather than being sprinkled through the domain model. ## Summary of the layered defence 1. **Compiler** — catches only an invalid string literal. 2. **go vet ./... in CI** — catches malformed tags, duplicate encoded names, and tags on unexported fields. 3. **A tag-assertion test** — catches wrong-but-well-formed tags, and freezes the contract. 4. **Review convention** — makes anyone changing a tag say why. Each layer catches a class the one above it cannot, and the class that reaches production is always the one that is syntactically perfect.
- Which struct tag mistakes does go vet's structtag check fail to catch?Anything well-formed: a misspelled key, the right key with the wrong value, a tag left on the wrong field after a refactor, or a value that was correct until the downstream contract moved. Vet validates the grammar and a few known keys' duplicates; it has no idea what your key means.
- How would you write a test that pins the tags on a struct?A table mapping field name to expected tag value, and a loop over the struct type's fields comparing each one. It fails the moment somebody edits a tag or reorders fields without updating the expectation, which turns an invisible character change into a visible diff the reviewer must approve.
- Why is a green go test run not enough protection here?go test runs only a subset of vet's analyzers over the package under test, so a malformed tag can pass through a fully green test run. Running go vet ./... as its own CI step covers every package with the full default analyzer set.
- How should a reviewer treat a pull request that changes only struct tags?As a contract change. Ask which consumer reads that key, whether the rename needs to be coordinated with a deployment order, and whether the type should be generated from the schema instead of hand-tagged. A tags-only diff is the easiest change in the world to wave through and one of the easier ones to break production with.
saying these in an interview costs you the question
- Says the compiler would have caught the tag typo
- Assumes a wrong tag key causes a run-time panic
- Thinks go vet validates what a tag key means
- Relies on go test to run every vet analyzer
- Treats a tags-only diff as cosmetic in review