skip to content

In Go, what does the compiler do with a struct field tag like schema:"user_id"?

level: middleimportance: should knowfreq 58%

answer

  1. the compiler only checks it parses
  2. an ordinary string literal, nothing more
  3. meaning arrives at run time
  4. key colon quoted value, space separated

basics

~20 s

Almost nothing: it checks the tag is a valid string literal and stores it with the field in the type's information. A tag is inert metadata, and only code that inspects the type at run time gives it any meaning.

solid answer

~50 s

A tag is an optional string literal that may follow a field's type in a struct declaration. The compiler records it as part of the type and otherwise ignores it — it never validates the contents, so any string at all is accepted. Meaning arrives entirely at run time, from libraries that inspect the type and pull out the key they care about. The convention the standard library follows is space-separated `key:"value"` pairs, written inside back quotes so the inner double quotes need no escaping, with the value free to carry comma-separated options. Tags are part of struct type identity, so two struct types differing only in their tags are distinct types, although Go lets you convert between them explicitly. Because nothing is checked at compile time, a misspelled key just means no library ever finds it.

code

go · 4 lines
go
type Field struct {
	Name string `schema:"name" doc:"column name"`
	Kind string `schema:"kind"`
}

go deeper

for a junior

Recognise the back-quoted string sitting after a field's type, and be able to say it is metadata for libraries to read rather than something the compiler acts on.

for a middle

Explain the conventional key:"value" grammar with space separation, that the whole tag is one string literal stored in the type, and that a tag applies to every name in the declaration it follows.

for a senior

Be ready to draw out the consequences of tags being unchecked strings: silent misspellings, tags forming part of type identity, and a tag edit on an exported struct being a contract change for whatever decodes it.

for a principal

Own the convention: which tag keys the codebase recognises, whether domain types carry transport tags at all or a separate wire struct does, and how that decision is kept consistent as teams add new keys.

## Where a tag lives in the grammar A struct field declaration is a list of names, a type, and then optionally a **tag**: ```go type Field struct { Name string `schema:"name" doc:"column name"` Kind string `schema:"kind"` } ``` The tag is an ordinary string literal. It is not a comment, not a directive, and not a declaration of anything. The compiler's only requirement is that it parse as a string literal; the bytes inside it are never inspected. Write `"anything at all"` there and the program still builds. Because the tag follows the whole field declaration, a tag on a multi-name declaration applies to every name in it: ```go type Point struct { X, Y int `schema:"coord"` // one tag, both fields carry it } ``` An embedded field can carry a tag too, and so can an unexported field — the compiler does not care, even though most libraries that read tags will only ever look at exported fields. ## Why back quotes The conventional grammar puts the value in double quotes, so writing the tag as a back-quoted **raw string literal** lets you type `schema:"user_id"` verbatim. An interpreted (double-quoted) literal is legal too, but then every inner quote needs a backslash, which nobody wants to read. The back quote is convention plus convenience, not a special syntax for tags. ## The conventional format Nothing in the language defines a structure for the tag string — the format is a convention that the standard library and essentially every third-party library follow: - one or more pairs, separated by **single spaces**; - each pair is `key:"value"`, with **no space after the colon**; - the key is a short package-ish name identifying the library that owns it; - the value is a string whose internal structure (commonly a name followed by comma-separated options) is that library's business. A lookup for a key that is not present simply returns nothing, and the library falls back to whatever its default is — usually the field's own name. Two libraries never collide, because each looks only for its own key. The keys are not registered anywhere the toolchain can see; they are a community convention. ## Tags and type identity This catches people out. Two struct types are identical only if they have the same field names, identical field types **and identical tags**. So: ```go type A struct{ N int `k:"1"` } type B struct{ N int `k:"2"` } ``` `A` and `B` are different types, and a value of one is not assignable to the other. Go does, however, allow an **explicit conversion** between struct types whose fields match apart from their tags — which is precisely how a wire-shaped struct with transport tags is converted into a bare domain struct of the same shape without copying field by field. Tags also become part of the identity of anonymous struct types, so an anonymous struct used as a temporary decoding target must carry the same tags as anything you plan to convert it to. ## Who reads a tag Only run-time type inspection can retrieve a tag: the reflect package exposes the tag string attached to a field and a helper that pulls a single key's value out of it, and that is the whole mechanism. There is no compile-time reflection in Go, no macro that could act on a tag, and no code generation built into the language. That is why tag-driven libraries pay a run-time cost the first time they inspect a type, and why many of them cache the derived plan per type. This also explains the ceiling on what tags can do. They can express **data about a field** — a wire name, a validation rule, a column — and they cannot express behaviour, because a string cannot hold code. Anything richer belongs in a method or in generated code. ## Practical consequences - **Tags are unchecked, so typos are silent.** Nothing rejects `schemaa:"user_id"`. The build is green and the field is quietly invisible to the library. - **Tags are part of your API when the struct is exported.** Changing a tag value on a struct that some other system decodes is a contract change dressed up as a one-word edit. - **Tags are visible in the binary.** They are type information, not comments, so they are not stripped and can be read by anything with the type. - **Tags do not compose.** Embedding a type does not merge its tags into the outer struct's own fields; the embedded field keeps its own. The mental model to leave with: a struct tag is a note pinned to a field, written in a format nothing enforces, addressed to a reader who may or may not turn up at run time.

  • Are two struct types that differ only in their field tags the same type?
    No. Tags are part of struct type identity, so the types are distinct and a value of one is not assignable to the other. Go does allow an explicit conversion between struct types that match apart from tags, which is how a tagged wire struct is turned into an untagged domain struct of the same shape.
  • Can an embedded field or a multi-name field declaration carry a tag?
    Yes to both. The tag follows the entire field declaration, so X, Y int with one tag gives both fields that tag, and an embedded field such as Base can carry a tag of its own. Embedding does not merge the embedded type's tags into the outer struct's fields.
  • Why are struct tags conventionally written in back quotes?
    Because the conventional format puts each value in double quotes. A back-quoted raw string literal lets you write the pairs verbatim. An interpreted literal is equally legal, but then every inner quote needs a backslash, which makes the tag much harder to read.

saying these in an interview costs you the question

  • Calls a struct tag an annotation the compiler acts on
  • Thinks the compiler validates tag keys or values
  • Says tags are comments stripped from the binary
  • Claims an unexported field cannot carry a tag
  • Believes embedding merges the embedded type's tags