Skip to content
AgileLogics
7 min read

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 vet catches them (structtag).
  • Calling Interface() or Set* on an unexported field — it panics.
  • Calling SetString on a value obtained from a non-pointer — panics with "unaddressable value".
  • Calling NumField on a pointer type instead of the struct — use Elem() 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]