Reflection & Struct Tags
Prerequisites: Iterators: range-over-func
The reflect package inspects types and values at runtime. It's what powers
encoding/json, database scanners, and validation libraries. Most
application code never needs it — but understanding it explains how struct
tags work.
Type and Value
reflect.TypeOf(x) describes the static type; reflect.ValueOf(x) holds the
value itself. Kind() reports the underlying category (Struct, Int,
Slice, Pointer, ...).
package main
import (
"fmt"
"reflect"
)
type UserID int
func main() {
var id UserID = 7
t := reflect.TypeOf(id)
fmt.Println(t.Name(), t.Kind())
v := reflect.ValueOf([]string{"a", "b"})
fmt.Println(v.Kind(), v.Len(), v.Index(1))
}
Expected output:
UserID int
slice 2 b
Struct tags
A struct tag is a string literal after a field. By convention it holds
space-separated key:"value" pairs, read with Field(i).Tag.Get(key):
package main
import (
"fmt"
"reflect"
)
type User struct {
Name string `json:"name" validate:"required"`
Email string `json:"email,omitempty" validate:"required"`
Age int `json:"age"`
}
func main() {
t := reflect.TypeOf(User{})
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Printf("%s json=%q validate=%q\n",
f.Name, f.Tag.Get("json"), f.Tag.Get("validate"))
}
}
Expected output:
Name json="name" validate="required"
Email json="email,omitempty" validate="required"
Age json="age" validate=""
The tag is just a string. Its meaning comes entirely from whichever package
reads it: encoding/json reads json, a validator reads validate.
A tiny validator
Combining field iteration with values lets you build a tag-driven check:
package main
import (
"errors"
"fmt"
"reflect"
)
type Signup struct {
Name string `validate:"required"`
Email string `validate:"required"`
Bio string
}
func Validate(v any) error {
rv := reflect.ValueOf(v)
if rv.Kind() == reflect.Pointer {
rv = rv.Elem()
}
if rv.Kind() != reflect.Struct {
return errors.New("validate: expected a struct")
}
rt := rv.Type()
var errs []error
for i := 0; i < rt.NumField(); i++ {
f := rt.Field(i)
if f.Tag.Get("validate") == "required" && rv.Field(i).IsZero() {
errs = append(errs, fmt.Errorf("%s is required", f.Name))
}
}
return errors.Join(errs...)
}
func main() {
fmt.Println(Validate(Signup{Name: "Ada"}))
fmt.Println(Validate(&Signup{Name: "Ada", Email: "ada@example.com"}))
}
Expected output:
Email is required
<nil>
Setting values
To modify a value through reflection you need an addressable value — pass a
pointer and call Elem(). CanSet() is false for unexported fields and for
values that aren't addressable:
u := User{Name: "Ada"}
v := reflect.ValueOf(&u).Elem()
v.FieldByName("Name").SetString("Grace")
fmt.Println(u.Name) // Grace
When to use reflection
Reach for it when writing a general-purpose library that must handle types it has never seen: serializers, ORMs, dependency containers. In application code prefer interfaces, generics, or plain code — reflection trades compile-time safety for runtime panics and is noticeably slower.
Common mistakes
- Malformed tags such as
json: "name"(space after the colon) or single quotes. They're silently ignored at runtime;go vetcatches them (structtag). - Calling
Interface()orSet*on an unexported field — it panics. - Calling
SetStringon a value obtained from a non-pointer — panics with "unaddressable value". - Calling
NumFieldon a pointer type instead of the struct — useElem()first.
Practice
Write Columns(v any) []string that returns the db tag of every field in a
struct, skipping fields whose tag is empty or "-". Test it on a Product
with fields ID (db:"id"), Name (db:"name"), and Cache (db:"-").
Show solution
package main
import (
"fmt"
"reflect"
)
type Product struct {
ID int `db:"id"`
Name string `db:"name"`
Cache string `db:"-"`
}
func Columns(v any) []string {
t := reflect.TypeOf(v)
if t.Kind() == reflect.Pointer {
t = t.Elem()
}
var cols []string
for i := 0; i < t.NumField(); i++ {
tag := t.Field(i).Tag.Get("db")
if tag == "" || tag == "-" {
continue
}
cols = append(cols, tag)
}
return cols
}
func main() {
fmt.Println(Columns(Product{}))
}
Expected output:
[id name]