32.3 Reading Struct Tags with reflect
Right, so you’ve got a struct. It’s a nice, tidy little struct. You’ve probably adorned it with some backticked decorations, like json:"name" or db:"user_id". These struct tags are fantastic for configuration, but they’re just… there. They’re a part of the comment, essentially invisible to your running code. Until you bring in the big guns: the reflect package.
Think of reflect as your all-access pass to the type system’s backstage. It lets you interrogate a type at runtime to figure out what it’s made of. And reading struct tags is one of its most common and practical party tricks. It’s how libraries like encoding/json know how to map your UserName field to a "user_name" key in JSON without you having to write a single line of parsing logic. Let’s break down how you can do that yourself.
The Basic Incantation
The process isn’t magic, but it is a bit ritualistic. You need to get the type of your object, get its underlying struct type (if it’s a pointer, you have to dereference it first), then iterate through its fields. For each field, you ask for its Tag.
package main
import (
"fmt"
"reflect"
)
type User struct {
ID int `json:"id" db:"user_id"`
Username string `json:"username" db:"user_name"`
Password string `json:"-"` // Ignore this in JSON
CreatedAt int64 `json:"created_at,omitempty"` // Omit if empty
}
func main() {
u := User{ID: 42, Username: "arthurdent"}
// Step 1: Get the "Value" of the variable
val := reflect.ValueOf(u)
// Step 2: Get the type from the value (a reflect.Type)
typ := val.Type()
// Step 3: Iterate over the fields of the struct
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i) // reflect.StructField
tag := field.Tag // reflect.StructTag (which is basically a string)
fmt.Printf("Field: %s\tTag: %s\n", field.Name, tag)
}
}
Running this will give you:
Field: ID Tag: json:"id" db:"user_id"
Field: Username Tag: json:"username" db:"user_name"
Field: Password Tag: json:"-"
Field: CreatedAt Tag: json:"created_at,omitempty"
Cool, we got the raw tag string. But we don’t want the whole thing; we usually want the value for a specific key, like json.
Actually Parsing the Tag Value
A reflect.StructTag is, under the hood, just a string with a fancy method bolted onto it: Get(key string) string. This method is the real workhorse. It understands the convention of spaces separating key-value pairs and will return the value for a specific key.
Let’s modify our loop to be more useful:
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
jsonTag := field.Tag.Get("json")
dbTag := field.Tag.Get("db")
fmt.Printf("Field: %s\tjson: %s\t\tdb: %s\n", field.Name, jsonTag, dbTag)
}
Now the output becomes genuinely useful:
Field: ID json: id db: user_id
Field: Username json: username db: user_name
Field: Password json: - db:
Field: CreatedAt json: created_at,omitempty db:
Notice what happened? For the Password field, the db tag doesn’t exist, so .Get returns an empty string. This is the correct way to check for the presence of a tag. You never check the raw tag string yourself.
Handling Pointers and Other Nasties
The code above works great for a concrete User struct. But what if you have *User? If you try to run reflect.ValueOf(&u).Type().NumField(), you’ll get a panic faster than you can say “nil pointer dereference.” This is because the type of a *User is a pointer, and a pointer has no fields.
You have to handle this. It’s a classic reflect footgun. The solution is to use Value.Elem() to dereference the pointer if it’s a pointer or interface, which gives you the underlying value.
func printTags(obj interface{}) {
val := reflect.ValueOf(obj)
// If it's a pointer, get the value it points to
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
// Now, get the type of the underlying value
typ := val.Type()
// Make sure we're actually dealing with a struct now.
// Trying to get fields from a slice or int would also panic.
if typ.Kind() != reflect.Struct {
fmt.Printf("Expected a struct, got %s\n", typ.Kind())
return
}
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
fmt.Printf("%s: %s\n", field.Name, field.Tag.Get("json"))
}
}
This function is now much more robust. It can handle User, *User, and even a nil *User (though val.Elem() will panic on a nil pointer, so you’d need an extra check for that—see what I mean about footguns?).
The Subtleties and “Why” Behind the Convention
You’ll see that the json tag for CreatedAt is "created_at,omitempty". The Get method returns the entire value: "created_at,omitempty". It’s up to the library using the tag (like encoding/json) to split this string on the comma and interpret the options. The reflect package itself doesn’t care about the content of the tag; it just provides the mechanism to read it. The meaning is defined by whatever library requested it.
This is why the convention is so powerful. It’s a generic key-value store slapped onto a struct field. The reflect package provides the standard way to read it, and then the consuming code is responsible for parsing the value according to its own rules. It’s a perfect example of separation of concerns.
So, the next time you slap a validate:"required,email" tag on a field, remember: you’re not using some magic language feature. You’re just leaving a note for some other code that knows how to use reflect to find it. And now, so do you.