10.6 Struct Tags: encoding/json, db, and Custom Tags
Now, let’s talk about struct tags, the little backtick-enclosed strings of metadata that make Go’s reflection magic actually useful. You’ve probably seen them hovering next to your struct fields like json:"name". They look like comments, but they are absolutely not. They’re a key part of your type’s definition, and the reflect package can read them. This is how we tell various encoders, ORMs, and other sorcerers how to handle our data.
Think of them as polite, strongly-worded suggestions you leave for other packages. “Hey, encoding/json? When you serialize this field, call the key "full_name", and if it’s empty, just leave it out entirely, would you? Thanks.”
The Syntax of a Tag
The syntax is simple, but everyone gets it wrong at least once. It’s a raw string literal (those backticks `) that lives after the field type. Inside, you provide key-value pairs. The key is usually the package namespace (like json), followed by a colon and a value inside double quotes.
type User struct {
ID int `json:"id"` // Simple key-value
Name string `json:"full_name,omitempty"` // Multiple options separated by commas
CreatedAt time.Time `json:"-"` // The hyphen means "ignore this field"
secretKey string `json:"secret_key"` // This will still be included! (unexported fields are ignored by json.Marshal)
}
The most common pitfall here? Using single quotes ' or forgetting the commas. The compiler won’t yell at you for a malformed tag; it’ll just silently ignore it, and you’ll spend 20 minutes wondering why your JSON output is wrong. I’ve been there. It’s not fun.
Mastering the encoding/json Tag
This is the big one. The encoding/json package uses these tags to map your wonderfully CamelCase Go field names to the often-snake_case world of JSON. Let’s break down its options:
json:"key_name": The basic remapping.Namebecomes"name",HTTPHandlerbecomes"httpHandler"(unless you tell it otherwise).json:"key_name,omitempty": Theomitemptyoption is a lifesaver. It tells the marshaller to skip this field if it’s a zero value for its type (empty string, 0, nil pointer, etc.). This keeps your JSON clean and lean.json:"-": The ultimate “ignore this” directive. The field won’t be read from or written to JSON. Essential for hiding internal state or fields that simply don’t belong in a serialized format (like a*sql.DBconnection).
Here’s the thing that trips people up: omitempty applies to the value, not the key. And it works with pointers beautifully. A nil pointer is an empty value, so it gets omitted. This is why you often see struct fields for optional values defined as pointers.
type Config struct {
Host string `json:"host"` // Always included, even if ""
Port int `json:"port,omitempty"` // Omitted if 0
User *string `json:"user,omitempty"` // Omitted if nil. Crucial for distinguishing between "not set" and "set to empty string".
}
func main() {
c := Config{Host: "", Port: 0, User: nil}
data, _ := json.Marshal(c)
fmt.Println(string(data)) // Output: {"host":""}
}
Other Common Tags: db, xml, and yaml
The same pattern repeats for other encoding and database packages. The db tag, used by libraries like sqlx and gorm, tells the SQL scanner how to map database column names to your struct fields.
type Product struct {
ID int `db:"id"` // maps to the 'id' column
Name string `db:"product_name"`
Price float64 `db:"price"`
IsAvailable bool `db:"is_available"`
}
Without these, sqlx would try to map the Name field to a column named name, which would fail if your DBA, in their infinite wisdom, named it product_name. The tag saves you from that particular frustration.
Creating and Using Your Own Custom Tags
Here’s where it gets really interesting. You’re not limited to the tags defined by other packages. You can use the reflect package to parse these tags for your own purposes. Maybe you have a custom validation framework, a configuration loader, or a documentation generator.
Let’s say you want to validate a struct based on rules in its tags.
type RegistrationForm struct {
Username string `mytags:"min=3,max=10,alphanum"`
Email string `mytags:"required,email"`
Age int `mytags:"min=18"`
}
func validateStruct(s interface{}) error {
v := reflect.ValueOf(s).Elem()
t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
tag := t.Field(i).Tag.Get("mytags")
if tag == "" {
continue
}
// Here you'd parse the tag string ("min=3,max=10")
// and write logic to validate the field based on the rules.
fmt.Printf("Field '%s' has rules: '%s'\n", t.Field(i).Name, tag)
}
return nil
}
func main() {
form := &RegistrationForm{Username: "al", Age: 16}
validateStruct(form)
// Output:
// Field 'Username' has rules: 'min=3,max=10,alphanum'
// Field 'Email' has rules: 'required,email'
// Field 'Age' has rules: 'min=18'
}
The key takeaway is that the tag is just a string. It’s up to your code to define the semantics, parse the options, and enforce the rules. It’s a powerful way to add metadata-driven behavior to your applications. Just don’t go overboard—you can easily create an unmaintainable tag-based DSL if you’re not careful.