skip to content

Using Go's reflect package, how do you list a struct's fields and read each field's struct tag?

level: juniorimportance: must knowfreq 50%

answer

  1. start from the type, not the value
  2. a count plus an indexed accessor
  3. each field arrives as one metadata struct
  4. the tag is a string with two methods
  5. Get takes the key you care about

basics

~10 s

reflect.TypeOf gives you the struct's Type; Type.NumField reports how many fields it declares and Type.Field(i) returns a StructField holding that field's Name, Type and Tag. Tag.Get reads one tag key out of it.

solid answer

~40 s

You walk the *type*, not the value. `t := reflect.TypeOf(User{})` gives a `reflect.Type`; if you started from a pointer you need `t = t.Elem()` first, because `NumField` panics unless the Kind is `Struct`. Then loop `for i := 0; i < t.NumField(); i++` and call `t.Field(i)`, which returns a `reflect.StructField` value carrying `Name`, `Type`, `Tag`, `Index`, `Offset`, `Anonymous` and `PkgPath`. The tag is a `reflect.StructTag`, a string with a conventional grammar, and `f.Tag.Get("db")` pulls out the value for one key. Fields come back in declaration order, which is what makes this workable for a generator that emits column mappings. Nothing here reads data: `StructField` is type metadata, so to touch actual values you would walk `reflect.ValueOf(x)` in parallel.

code

go · 13 lines
go
type User struct {
	ID	int	`db:"user_id"`
	Email	string	`db:"email"`
}

t := reflect.TypeOf(User{})
for i := 0; i < t.NumField(); i++ {
	f := t.Field(i)
	fmt.Println(f.Name, f.Type, f.Tag.Get("db"))
}
// prints:
// ID int user_id
// Email string email

go deeper

for a junior

Be ready to write the loop from memory: reflect.TypeOf, NumField, Field(i), then Tag.Get with a key. Know that a StructField describes a field and does not hold its data.

for a middle

Explain why the walk lives on reflect.Type rather than reflect.Value, what each StructField member means, and why NumField panics unless the Kind is Struct.

for a senior

Show judgment about where a reflect walk belongs: once at start-up or inside a generator, not on a hot per-request path, and always behind a guard that rejects non-struct input clearly.

for a principal

Frame reflection as a dependency on a type's shape that no compiler will check for you. Argue when a generated mapper or type parameters serve the team better than a runtime walk.

## The two halves of reflect Go's `reflect` package exposes a value's runtime type and its runtime data as two separate objects: `reflect.Type` (what the thing *is*) and `reflect.Value` (what the thing *holds*). Walking a struct's shape and reading its tags is entirely a `Type` job — the tags and the field list belong to the type, not to any particular instance. That is why a code generator can work from `reflect.TypeOf(User{})` on a zero value it never fills in. ## Getting a struct Type `reflect.TypeOf(x)` takes an `any` and returns the dynamic type of what was passed. If you pass `User{}` you get the struct type directly. If you pass `&User{}` you get a pointer type, and every struct-shaped method on it panics; call `t.Elem()` to step from `*User` to `User`. Defensive walkers usually normalise first: ```go for t.Kind() == reflect.Pointer { t = t.Elem() } if t.Kind() != reflect.Struct { return fmt.Errorf("want a struct, got %s", t) } ``` The guard matters because `NumField`, `Field`, `FieldByName` and friends are documented to panic when the Kind is not `Struct`. Reflection reports misuse by panicking, not by returning errors. ## NumField and Field `t.NumField()` returns the number of fields the struct *declares*. `t.Field(i)` returns the i-th of them, in declaration order, as a `reflect.StructField` — note that this is a plain struct value, not an interface, so you can copy it around freely. Its useful members are: - `Name string` — the field's identifier as written in the source. - `Type reflect.Type` — the field's type, which you can recurse into. - `Tag reflect.StructTag` — the raw back-quoted tag string, or `""` if none was written. - `Index []int` — the index path used by `FieldByIndex`; a single element for a directly declared field. - `Anonymous bool` — true when the field is embedded rather than named. - `PkgPath string` — empty for an exported field, and the declaring package's path for an unexported one. - `Offset uintptr` — the byte offset within the struct, occasionally useful to low-level code. ## Reading the tag `reflect.StructTag` is a defined string type with two methods. `Get(key)` returns the value for a key as a plain string, and `Lookup(key)` returns the value plus a boolean saying whether the key was there at all. The conventional format the parser understands is a space-separated list of `key:"value"` pairs, where the value is a quoted Go string literal: ```go ID int `db:"user_id" validate:"required"` ``` Within a value, packages conventionally use commas to add options — `db:"user_id,pk"` — but that is *convention only*. `reflect` hands you the whole value string, `user_id,pk`, and splitting it is the consuming package's job. The compiler stores the tag as an opaque string in the type's metadata; it never validates its contents. ## A complete walk ```go t := reflect.TypeOf(User{}) for i := 0; i < t.NumField(); i++ { f := t.Field(i) fmt.Printf("%s %s %q\n", f.Name, f.Type, f.Tag.Get("db")) } ``` That loop is the backbone of every tag-driven mapper: an encoder deciding what to emit, a validator deciding what to check, or a generator emitting one SQL column per tagged field. ## Things that surprise people The walk sees the struct's *declared* fields, so an embedded struct arrives as one field whose own fields are not counted; reaching those means recursing, or calling `reflect.VisibleFields`, which performs the breadth-first walk and returns promoted fields too. A field with no tag is not an error — `Tag` is simply the empty string and `Get` returns `""`. And an unexported field still appears in the walk: you can read its name, type and tag from the `StructField`, even though `reflect` will not hand you its value. ## Cost A reflect walk is far slower than direct field access — it allocates, it defeats inlining, and it does string work on every tag read. That is fine at start-up or in a generator that runs once, and it is worth measuring before you put it on a per-request path.

  • What happens if you call NumField on a reflect.Type whose Kind is Pointer?
    It panics. `reflect.TypeOf(&User{})` gives you `*User`, and the struct-shaped methods are only valid when the Kind is `Struct`. Call `t.Elem()` to step through the pointer first, usually in a small loop so a `**T` is handled too, and guard with a `t.Kind() != reflect.Struct` check before you walk. Reflection reports this class of mistake by panicking rather than by returning an error.
  • Does Type.Field(i) give you the field's value as well as its metadata?
    No. `Type.Field(i)` returns a `reflect.StructField`, which is pure type metadata: name, type, tag, index, offset. To reach data you walk a `reflect.Value` in parallel — `reflect.ValueOf(x).Field(i)` — using the same index. Keeping the two straight matters because a generator that only emits mappings never needs a `Value` at all; it can work from the type of a zero struct.
  • Does the compiler check what you write inside a struct tag?
    No. To the compiler a tag is an opaque string literal attached to the field and stored in the type's metadata; any string is legal. Only `reflect` parses it, and only by convention. That is why a typo inside a tag compiles happily and fails silently at runtime, and why `go vet`'s structtag analyzer exists to catch tags that `reflect.StructTag.Get` will not be able to parse.

reflect.Type is the blueprint in the filing cabinet, not the building. NumField and Field let you read the blueprint's list of rooms and the label stuck to each door.

saying these in an interview costs you the question

  • Thinks Type.Field returns the field's value
  • Calls NumField on a pointer type without Elem
  • Says the compiler validates struct tag contents
  • Believes tags exist only for encoding/json
  • Expects Field to iterate in alphabetical order