skip to content

What does StructField.Tag.Get return when a struct tag is malformed, and how does Tag.Lookup differ?

level: middleimportance: should knowfreq 42%

answer

  1. one returns a string, one returns a pair
  2. three causes, one empty result
  3. the parser gives up quietly
  4. no error is ever returned
  5. vet has an analyzer for exactly this

basics

~20 s

Get returns the empty string in three different situations: the key is absent, its value is genuinely empty, or the tag's syntax broke so parsing stopped early. Lookup returns the value plus an ok flag, which separates absent from present-but-empty.

solid answer

~50 s

`reflect.StructTag.Get` never reports a problem — it returns `""` for a key that is not there, for a key whose value is the empty string, and for a tag whose syntax the parser could not follow. The parser expects a space-separated run of `key:"value"` pairs with the value written as a quoted Go string; the moment a segment does not fit, it stops, so every key after the broken one becomes invisible. `Lookup` returns `(value, ok)` and buys you exactly one extra bit: it distinguishes `db:""` from no `db` key at all. It still cannot tell you the tag was garbage — an unparseable tag looks identical to an absent key. Neither method ever returns an error, so the real defence is `go vet`, whose structtag analyzer flags tags that `Get` will not be able to parse.

code

go · 10 lines
go
type Row struct {
	ID	int	`db:"id"`
	Name	string	`db: "name"` // space after the colon
}

f := reflect.TypeOf(Row{}).Field(1)
fmt.Println(f.Tag.Get("db") == "") // true

v, ok := f.Tag.Lookup("db")
fmt.Println(v == "", ok) // true false

go deeper

for a junior

Know that Get returns a plain string and never an error, and that Lookup's second return value tells you whether the key was present at all.

for a middle

Explain the key:"value" grammar, why parsing stops at the first malformed pair, and why three different causes all surface as an empty string.

for a senior

Show how you keep a silent tag typo out of production: Lookup plus a hard error in the walker, go vet in CI, and a test asserting the expected mapping.

for a principal

Argue for where tag correctness is enforced across a codebase - vet in the pipeline, generated code checked in and diffed, or a schema test - rather than trusting reviewers to spot a stray space.

## The grammar reflect expects A struct tag is, to the compiler, an opaque string. `reflect` imposes a convention on top of it: a sequence of `key:"value"` pairs, separated by spaces, where the key is a run of characters that are not spaces, quotes, colons or control characters, and the value is a Go quoted string literal. ```go type Row struct { ID int `db:"id" validate:"required"` } ``` That is the whole grammar. Options like `,pk` or `,omitempty` are *not* part of it: they live inside the value, and every consuming package splits them itself. `reflect` hands back `id,pk` as one string and takes no view on the comma. ## How the parser fails `StructTag.Get` and `StructTag.Lookup` walk the tag left to right: skip spaces, scan a key up to the colon, then read a quoted string. If any step does not match the shape it expects, parsing simply stops and the key is reported as not found. There is no error return and no panic. Two consequences follow: 1. A single broken segment hides everything after it. In `` `db: "id" json:"id"` `` the space after the first colon breaks the first pair, so `Get("json")` also comes back empty even though the `json` pair itself is written correctly. 2. Every failure mode collapses onto the same empty string. Common ways to break a tag: a space after the colon; separating pairs with commas instead of spaces; forgetting the quotes around the value; writing the tag with regular double quotes instead of back quotes and then not escaping the inner quotes. ## What Lookup adds ```go value, ok := f.Tag.Lookup("db") ``` `ok` is true only when the key was found. This lets you separate three states that `Get` flattens into one: - `ok == false` — no `db` key (or the tag was unparseable, which is indistinguishable). - `ok == true, value == ""` — the field carries `db:""`, an explicit empty value. - `ok == true, value != ""` — a real mapping. That middle case matters more than it looks. A mapper that treats `db:""` as "no tag" and a mapper that treats it as "map to a column named empty string" behave differently, and only `Lookup` lets you choose deliberately. In any tag-driven tool, `Lookup` should be the default and `Get` the shortcut you use when you have already decided that absent and empty mean the same thing. ## Why this is a real production failure Picture a code generator that walks a struct and emits one SQL column per `db` tag. Someone renames a database column and edits the tag, and in the process types `db :"account_id"` or `db: "account_id"`. Everything compiles — the tag is a legal string. The generator's `Get` returns `""`, so the field is quietly dropped from the generated mapping, and from then on reads leave that field at its zero value and writes never touch the column. Nothing panics, nothing logs; the bug shows up as data that is silently wrong. ## The defences **`go vet`.** Its structtag analyzer validates that each tag can actually be parsed by `reflect.StructTag.Get`, and reports bad syntax as well as duplicate names in the tags it understands. `go vet` runs automatically as part of `go test`, so the check costs nothing if you keep the vet output clean; a CI job that runs `go vet ./...` catches the typo at review time rather than in production. **Use `Lookup` and fail.** A generator that returns an error listing every exported field with no usable tag turns a silent drop into a build failure. This is the single highest-value line of code in a tag-driven tool. **Test the walk.** A table test that asserts the expected set of column names for a representative struct catches renames that vet cannot see, because a *correctly formed* tag with the wrong value is not a syntax error. ## What neither method does Neither `Get` nor `Lookup` validates the value, splits options, canonicalises case, or resolves promoted fields from embedded structs. They are string lookups over a tiny grammar. All policy — what a missing tag means, what the comma-separated options mean, whether an unexported field may carry a tag at all — belongs to the code doing the walking.

  • Can Lookup tell you that a struct tag is syntactically broken?
    No. Lookup only reports whether the key was found. An unparseable tag and an absent key both come back as `("", false)`, so a walker cannot distinguish a typo from a deliberately untagged field. That is why syntax checking has to happen elsewhere: `go vet`'s structtag analyzer parses tags the same way reflect does and reports the ones that will not survive it, and it runs by default under `go test`.
  • Where do comma-separated options like the ,pk in db:"user_id,pk" fit into the grammar reflect parses?
    They do not — reflect stops at the value. `Get("db")` returns the whole string `user_id,pk`, and it is the consuming package that splits on commas and assigns meaning to each option. Different packages define different option vocabularies over the same mechanism, which is why two libraries can read the same tag key and disagree about what its options mean.
  • What happens if you separate two tag pairs with a comma instead of a space?
    Parsing derails. After the first quoted value the parser expects spaces then a key, and a comma becomes part of what it tries to read as the next key name, so the second pair is not found under the name you intended. As always there is no error: the key simply reports as absent, and `go vet` is what tells you the tag is malformed.

saying these in an interview costs you the question

  • Thinks Get returns an error for a broken tag
  • Assumes an empty Get result means the key is absent
  • Believes the compiler rejects a malformed struct tag
  • Thinks reflect splits the comma options for you
  • Assumes a broken pair only affects its own key