skip to content

When you walk a struct with reflect, how do embedded fields appear and how do you reach their promoted fields?

level: middleimportance: should knowfreq 34%

answer

  1. fewer declared fields than nameable ones
  2. one field, with a flag set
  3. Index is a path, not a number
  4. breadth-first, shallowest wins
  5. one reflect function does the whole walk

basics

~10 s

An embedded struct counts as one field: Type.Field(i) returns it with Anonymous true and Name equal to the embedded type's name. Its inner fields are not in NumField, so recurse or call reflect.VisibleFields.

solid answer

~40 s

`NumField` counts declared fields, and an embedded struct is one of them. It arrives as a `StructField` whose `Anonymous` is true and whose `Name` is the embedded type's name, carrying its own tag if one was written. The fields promoted from inside it are not in the top-level walk at all. Two ways to reach them: recurse yourself when `f.Anonymous && f.Type.Kind() == reflect.Struct` (remembering an embedded `*T` needs an `Elem()` first), or call `reflect.VisibleFields(t)`, which does the breadth-first walk and returns the promoted fields as well. Either way, a promoted field's `StructField.Index` is a path such as `[0 0]` rather than a single number, and `Value.FieldByIndex` walks that path — `FieldByIndexErr` returns an error instead of panicking when it has to step through a nil embedded pointer.

code

go · 15 lines
go
type Audit struct {
	CreatedBy	string	`db:"created_by"`
}

type User struct {
	Audit
	Email	string	`db:"email"`
}

t := reflect.TypeOf(User{})
fmt.Println(t.NumField())         // 2
fmt.Println(t.Field(0).Anonymous) // true

f, ok := t.FieldByName("CreatedBy")
fmt.Println(ok, f.Index, f.Tag.Get("db")) // true [0 0] created_by

go deeper

for a junior

Remember that an embedded struct shows up as one field with Anonymous true, and that the fields inside it are not part of the outer NumField count.

for a middle

Explain how to recurse safely - only on anonymous fields, dereferencing an embedded pointer type - and what reflect.VisibleFields does instead.

for a senior

Be ready to say what your walker does when embedding creates duplicate names or shadowing, and how you keep a newly embedded type from silently changing generated output.

for a principal

Own the flattening policy for a shared tag-driven library: prefixing, shadowing and duplicate handling are contract decisions that importers will depend on and you cannot quietly change later.

## What the walk actually sees Embedding writes a type into a struct with no field name: ```go type Audit struct { CreatedBy string `db:"created_by"` } type User struct { Audit Email string `db:"email"` } ``` `reflect.TypeOf(User{}).NumField()` is **2**, not 3. `Field(0)` is the `Audit` field itself: `Name` is `"Audit"` (the type's name stands in for the field name), `Anonymous` is true, `Type` is the `Audit` struct type, and `Tag` is whatever was written on the embedding line — embedding can carry its own tag, and that tag has nothing to do with the tags inside `Audit`. This trips up tag-driven tools written without embedding in mind. A generator that emits one column per `db` tag sees two fields, finds a `db` tag on only one of them, and produces a mapping that is missing `created_by` — silently, because a struct field with no `db` tag is not an error condition in most walkers. ## Recursing yourself The manual fix is to descend when the field is embedded and struct-shaped: ```go for i := 0; i < t.NumField(); i++ { f := t.Field(i) if f.Anonymous { ft := f.Type if ft.Kind() == reflect.Pointer { ft = ft.Elem() } if ft.Kind() == reflect.Struct { walk(ft) // collect the promoted fields too continue } } // ordinary field } ``` Two details matter. An embedded field need not be a struct — you can embed an interface or a named non-struct type — so guard on the Kind before recursing. And you must recurse only on `Anonymous` fields: a *named* struct field is a nested value, not a promotion, and flattening it would invent columns the language never promoted. ## Letting reflect do it `reflect.VisibleFields(t)` returns all the fields that are visible on `t`, in breadth-first order, including the embedded struct fields themselves and the fields promoted through them. Each returned `StructField` has an `Index` describing how to get there. It applies Go's own visibility rules, which is its real advantage: shadowing and ambiguity are resolved the way the language resolves them, not the way you re-implemented them. ## Index and FieldByIndex For a directly declared field, `Index` has one element. For a promoted field it is a path: `CreatedBy` in the example above has `Index == []int{0, 0}` — field 0 of `User`, then field 0 of `Audit`. `Type.FieldByIndex(index)` and `Value.FieldByIndex(index)` follow that path. On the value side there is a hazard: if a step is an embedded *pointer* that happens to be nil, `Value.FieldByIndex` panics, because it would have to allocate to continue. `Value.FieldByIndexErr` exists precisely for that case and returns an error instead. ## FieldByName and ambiguity `Type.FieldByName("CreatedBy")` returns `(StructField, bool)` and does resolve promoted names, applying the same rules the compiler applies to a selector: the shallowest depth wins, and if two fields at the same shallowest depth have the name, the selector is ambiguous and the lookup reports `false` rather than picking one. Shadowing works the same way — a `CreatedBy` field declared directly on `User` hides the promoted one, and `FieldByName` returns the outer one with a one-element `Index`. ## Policy questions a walker has to answer Once embedding is in play, a tag-driven tool has to make explicit decisions that were invisible before: does an embedded struct flatten into the parent's namespace, or does it get a prefix? Does an embedded unexported type still contribute its exported promoted fields? What if the same tag value appears twice after flattening — is that an error, or does the shallower one win? None of these have a single right answer, but a walker that never states its answer produces output people cannot predict, and the surprises land months later when someone adds an embedded type to a struct that has always worked.

  • What does StructField.Index hold for a promoted field, and what consumes it?
    It holds the sequence of field indices needed to reach the field from the outer struct — `[]int{0, 0}` means field 0 of the outer type, then field 0 of that. `Type.FieldByIndex` and `Value.FieldByIndex` follow the path. On a value, if a step is a nil embedded pointer, `FieldByIndex` panics; `FieldByIndexErr` returns an error for that case instead.
  • Two embedded structs both have a field named ID at the same depth. What does Type.FieldByName("ID") return?
    It returns `ok == false`. FieldByName applies the language's selector rules: the shallowest depth wins, but when two candidates tie at the shallowest depth the name is ambiguous and no field is chosen. That mirrors the compile error you would get writing `u.ID` in source, except that reflect reports it as a failed lookup at runtime rather than as a build failure.
  • Can the embedding line itself carry a struct tag, and what does it mean?
    Yes - writing a `db:"audit"` tag on the embedding line is legal, and it lands on the embedded field's own StructField. It means nothing on its own; it is metadata for whichever package reads it. Consumers differ: some use it to prefix or rename the promoted fields, others ignore it entirely. If your walker gives it a meaning, document it, because readers cannot infer it from the language.

saying these in an interview costs you the question

  • Expects NumField to count promoted fields
  • Recurses into every struct field, not just anonymous ones
  • Treats StructField.Index as a single int
  • Assumes a tag on the embedding line applies to inner fields
  • Forgets an embedded pointer type needs Elem before recursing