Struct Tags and reflect
Struct Tags and reflect
Struct tags are Go’s back door for tooling. Gortex’s declarative routing reads them with reflect to turn a struct into a whole routing table. This post lays the groundwork first.
Reading a struct tag
A struct tag is the raw string after a field, conventionally space-separated key:"value" pairs:
type User struct {
Name string `json:"name" validate:"required"`
}
The compiler doesn’t interpret it; you read it at runtime with reflect:
f, _ := reflect.TypeOf(User{}).FieldByName("Name")
f.Tag.Get("json") // "name"
v, ok := f.Tag.Lookup("xml") // "", false
The Get vs Lookup distinction matters: Get returns "" for both “key absent” and “key present but empty”, so it can’t tell them apart; Lookup adds a bool. Gortex detects inject:"" (an empty-value tag) with Lookup, or it would mistake “deliberately empty” for “no tag at all”.
reflect’s trio: Type / Value / Kind
reflect.Type: the static structure — field count, names, tags.reflect.Value: the runtime value, readable and writable.Kind: the underlying category (Ptr,Struct,String…), distinct from the named type.type UserID inthas KindInt.
v := reflect.ValueOf(&User{})
v.Kind() // reflect.Ptr
v.Elem().Kind() // reflect.Struct; Elem() dereferences one pointer
t := v.Elem().Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
_ = field.Tag.Get("json")
}
Walking safely, checking signatures
reflect doesn’t return an error on misuse — it panics. Check before you reach:
- Verify
Kindfirst: don’t callNumField()on a non-struct, orElem()on a non-pointer. - Skip unexported fields with
field.CanInterface()(orStructField.IsExported()), or reading the value panics. - Before calling a method, get it via
MethodByName, inspectMethod.Type(NumIn/NumOut) to confirm the signature, thenCall.
This is exactly what B2’s route registration does: scan fields for GET / POST methods, check the signature, then register.
The cost of reflection
reflect is slow (it bypasses static dispatch), sidesteps compile-time type checking, and panics at runtime. The rule is one line: use it at startup / config time, keep it out of the hot path. That’s gortex’s split — reflect scans routes at startup for the dev experience, a production build swaps in codegen, and no request touches reflect (see B2, B4).
From Effective Go: blank identifier, Data
The blank identifier _ gets a whole section in Effective Go (The blank identifier): discarding unwanted return values, for range when you only want the index, and the interface check —
var _ io.Writer = (*MyType)(nil) // compile-time proof that *MyType implements io.Writer
Data covers allocation: new(T) returns a *T to zeroed memory, make is reserved for slices / maps / channels. reflect.New is the new equivalent, and B2 uses it to auto-initialise nil handler fields.
Takeaways
- A struct tag is the raw string after a field; reflect reads it with
Get/Lookup, andLookupdistinguishes “absent” from “present but empty”. - The trio is Type / Value / Kind; Kind is the underlying category, independent of the named type.
- reflect panics on misuse: check Kind first, skip unexported fields, verify a method signature before calling.
- reflect is slow and unsafe: use it at startup, never on the request path.
- Struct tags + reflect are the groundwork for B2 (Struct-Tag Routing,
gortex-struct-tag-routing).
Further reading: the series goes on to dissect the gortex framework built on these ideas — source at yshengliao/gortex.
Outline by Sheng, drafted with Claude · Go 1.24 (latest release at the time) · compiled retroactively · part of the 2026-06-13 blog renovation, paint still drying.