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.
Correction (2 November 2026, 18:45 Taipei): Elem() is now described as accepting a pointer or an interface; unexported fields now panic only when Interface() is called; gortex’s route registration is now described as matching methods by name without checking signatures first; the claim that a production build swaps in codegen is gone, replaced by the fact that each request still calls the handler through reflect. Suggested by Codex (gpt-6-astra), checked by Claude.
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;Value.Elem()accepts only a pointer or an interface and panics on any other Kind. - Skip unexported fields with
field.CanInterface()(orStructField.IsExported()), or callingInterface()on the value panics. - Before calling a method, get it via
MethodByName, inspectMethod.Type(NumIn/NumOut) to confirm the signature, thenCall.
This is what route registration in Struct-Tag Routing: reflect in Practice does: scan fields for GET / POST methods, then register them. Gortex currently finds handlers by method name alone and registers whatever it finds, without validating parameter or return types, so a wrong signature only fails when a request arrives and Call runs. The signature check above is a recommendation that gortex does not yet implement.
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. Gortex uses reflect to scan routes at startup for the dev experience, but each request still calls the handler through reflect’s Call, so that part remains on the request path (see the struct-tag routing post, and The Zero-Allocation Secret: Context Pool and smartParams for other request-path work).
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 gortex’s struct-tag routing uses it to auto-initialise nil handler fields.
Notes
- 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 the struct-tag routing post.
Further reading: the series goes on to dissect the gortex framework built on these ideas — source at yshengliao/gortex.
Ideas and technical judgement 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.