Struct tags provide a way to attach metadata to struct fields. They’re essential for serialization, validation, and database mapping. Go packages like encoding/json, encoding/xml, and ORM libraries all rely on struct tags to map fields between Go and external representations. For a broader view of how Go handles data transformation, see Encoding and Decoding Data.
Struct Tag Basics
Tags are string literals attached to struct fields following the field type declaration. Each tag is a raw string (key:"value") that packages parse using reflect — the same mechanism used for Reflection and Type Inspection.
package main
import (
"encoding/json"
"fmt"
"log"
)
type Person struct {
Name string `json:"name"`
Age int `json:"age"`
Email string `json:"email,omitempty"`
}
func main() {
p := Person{Name: "Alice", Age: 30, Email: "[email protected]"}
data, err := json.Marshal(p)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
var p2 Person
if err := json.Unmarshal([]byte(`{"name":"Bob","age":25}`), &p2); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", p2)
}
Output:
{"name":"Alice","age":30,"email":"[email protected]"}
{Name:Bob Age:25 Email:}
Common Tag Types
JSON Tags
Control how struct fields appear in JSON output. Use json:"-" to exclude fields and omitempty to skip zero-value fields.
package main
import (
"encoding/json"
"fmt"
"log"
)
type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
Hidden string `json:"-"` // Ignored
Optional string `json:"optional,omitempty"` // Omitted if empty
}
func main() {
p := Product{
ID: 1,
Name: "Widget",
Price: 9.99,
Hidden: "secret",
}
data, err := json.MarshalIndent(p, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
}
Output:
{
"id": 1,
"name": "Widget",
"price": 9.99
}
For more on JSON handling, see Working with JSON in Go.
Database Tags
ORM libraries and database mappers use struct tags to map struct fields to database columns:
package main
import (
"fmt"
)
type User struct {
ID int `db:"id" json:"id"`
Name string `db:"name" json:"name"`
Email string `db:"email" json:"email"`
Age int `db:"age" json:"age"`
}
func main() {
u := User{ID: 1, Name: "Alice", Email: "[email protected]", Age: 30}
fmt.Printf("%+v\n", u)
}
Validation Tags
Validation libraries like go-playground/validator use tags to define field validation rules:
package main
import (
"fmt"
)
type Account struct {
Username string `validate:"required,min=3,max=20"`
Email string `validate:"required,email"`
Age int `validate:"required,min=18,max=120"`
}
func main() {
a := Account{
Username: "alice",
Email: "[email protected]",
Age: 30,
}
fmt.Printf("%+v\n", a)
}
Parsing Tags
Extract and use tag information at runtime using the reflect package:
package main
import (
"fmt"
"reflect"
"strings"
)
type Person struct {
Name string `json:"name" validate:"required"`
Age int `json:"age" validate:"required,min=0,max=150"`
Email string `json:"email" validate:"email"`
}
func parseTags(v interface{}) {
t := reflect.TypeOf(v)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
jsonTag := field.Tag.Get("json")
validateTag := field.Tag.Get("validate")
fmt.Printf("Field: %s\n", field.Name)
fmt.Printf(" JSON: %s\n", jsonTag)
fmt.Printf(" Validate: %s\n", validateTag)
if validateTag != "" {
rules := strings.Split(validateTag, ",")
fmt.Printf(" Rules: %v\n", rules)
}
}
}
func main() {
p := Person{}
parseTags(p)
}
Output:
Field: Name
JSON: name
Validate: required
Rules: [required]
Field: Age
JSON: age
Validate: required,min=0,max=150
Rules: [required min=0 max=150]
Field: Email
JSON: email
Validate: email
Rules: [email]
reflect.TypeOf inspects the struct’s type information, and field.Tag.Get("key") returns the value for a specific tag key. If a tag key is missing, it returns an empty string.
Custom Tag Handling
Create and use custom tags for domain-specific metadata like configuration defaults:
package main
import (
"fmt"
"reflect"
)
type Config struct {
Host string `config:"host" default:"localhost"`
Port int `config:"port" default:"8080"`
Debug bool `config:"debug" default:"false"`
Timeout int `config:"timeout" default:"30"`
}
func loadConfig(v interface{}) map[string]interface{} {
result := make(map[string]interface{})
t := reflect.TypeOf(v)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
configName := field.Tag.Get("config")
defaultVal := field.Tag.Get("default")
if configName != "" {
result[configName] = defaultVal
}
}
return result
}
func main() {
cfg := Config{}
config := loadConfig(cfg)
for key, val := range config {
fmt.Printf("%s: %v\n", key, val)
}
}
This pattern lets you build custom configuration loaders, form mappers, or serialization layers without external libraries.
Tag-Based Validation
Validate struct fields at runtime by parsing validation rules from struct tags:
package main
import (
"fmt"
"reflect"
"strconv"
"strings"
)
type User struct {
Name string `validate:"required,min=3,max=50"`
Age int `validate:"required,min=0,max=150"`
Email string `validate:"required"`
}
func validate(v interface{}) []string {
var errors []string
t := reflect.TypeOf(v)
val := reflect.ValueOf(v)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fieldVal := val.Field(i)
validateTag := field.Tag.Get("validate")
if validateTag == "" {
continue
}
rules := strings.Split(validateTag, ",")
for _, rule := range rules {
parts := strings.Split(rule, "=")
ruleName := parts[0]
switch ruleName {
case "required":
if fieldVal.IsZero() {
errors = append(errors, fmt.Sprintf("%s is required", field.Name))
}
case "min":
if len(parts) > 1 {
minVal, _ := strconv.Atoi(parts[1])
if fieldVal.Kind() == reflect.Int && fieldVal.Int() < int64(minVal) {
errors = append(errors, fmt.Sprintf("%s must be >= %d", field.Name, minVal))
}
}
case "max":
if len(parts) > 1 {
maxVal, _ := strconv.Atoi(parts[1])
if fieldVal.Kind() == reflect.Int && fieldVal.Int() > int64(maxVal) {
errors = append(errors, fmt.Sprintf("%s must be <= %d", field.Name, maxVal))
}
}
}
}
}
return errors
}
func main() {
u := User{Name: "Al", Age: 200}
errors := validate(u)
for _, err := range errors {
fmt.Println("Error:", err)
}
}
Output:
Error: Name must be >= 3
Error: Age must be <= 150
Error: Email is required
This is a simplified validation engine — production code should use libraries like go-playground/validator which handle nested structs, custom types, and internationalization.
Multiple Tags
Use multiple tags on a single field for different concerns (JSON serialization, database mapping, CSV export):
package main
import (
"encoding/json"
"fmt"
"log"
"reflect"
)
type Product struct {
ID int `json:"id" db:"product_id" csv:"id"`
Name string `json:"name" db:"product_name" csv:"name"`
Price float64 `json:"price" db:"product_price" csv:"price"`
}
func extractTags(v interface{}) {
t := reflect.TypeOf(v)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
fmt.Printf("Field: %s\n", field.Name)
fmt.Printf(" JSON: %s\n", field.Tag.Get("json"))
fmt.Printf(" DB: %s\n", field.Tag.Get("db"))
fmt.Printf(" CSV: %s\n", field.Tag.Get("csv"))
}
}
func main() {
p := Product{}
extractTags(p)
p = Product{ID: 1, Name: "Widget", Price: 9.99}
data, err := json.Marshal(p)
if err != nil {
log.Fatal(err)
}
fmt.Println("JSON:", string(data))
}
Best Practices
Well-Structured Tags
package main
import (
"encoding/json"
"fmt"
"log"
)
type Article struct {
ID int `json:"id" db:"id" csv:"id"`
Title string `json:"title" db:"title" csv:"title" validate:"required,min=5"`
Content string `json:"content" db:"content" csv:"content" validate:"required"`
Author string `json:"author" db:"author" csv:"author"`
Published bool `json:"published" db:"published" csv:"published"`
}
func main() {
a := Article{
ID: 1,
Title: "Go Struct Tags",
Content: "Learn about struct tags...",
Author: "Alice",
Published: true,
}
data, err := json.MarshalIndent(a, "", " ")
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
}
Avoid: Inconsistent Tag Naming
// BAD: inconsistent naming across tags — "id" vs "ID" vs "ID"
type User struct {
ID int `json:"id" db:"user_id" csv:"ID"`
Name string `json:"name" db:"user_name" csv:"NAME"`
Email string `json:"email" db:"user_email" csv:"EMAIL"`
}
Mixing snake_case, PascalCase, and UPPER_CASE across tags for the same field makes code harder to maintain and can cause subtle mapping bugs when switching between serialization formats.
Common Tag Conventions
package main
type Example struct {
// JSON serialization
Field1 string `json:"field1"`
// Database mapping
Field2 string `db:"field_2"`
// CSV export
Field3 string `csv:"field3"`
// Validation rules
Field4 string `validate:"required,email"`
// XML serialization
Field5 string `xml:"field5"`
// YAML serialization
Field6 string `yaml:"field6"`
// Ignored fields in serialization
Field7 string `json:"-" db:"-"`
}
Best Practices
- Use Standard Tags: Prefer
json,db,xml,yaml— avoid inventing custom tag formats when standard ones exist. - Be Consistent: Use the same naming convention across all tags for a given field (e.g., always
snake_case). - Document Custom Tags: Explain the meaning and expected values of any custom tags you introduce.
- Validate Tags: Check tag syntax at startup or in tests to catch typos early.
- Cache Reflection: Parse tags once and cache results — don’t re-parse on every request.
- Use
omitempty: For optional JSON fields to keep output clean. - Handle Errors: Always check errors from serialization functions.
- Test Tags: Write unit tests that exercise tag-based behavior for each struct.
Common Pitfalls
- Typos in Tags:
json:"name"vsjson:"nme"— no compile-time check, use tests. - Inconsistent Naming: Mixing conventions across tags makes maintenance harder.
- Ignoring Errors: Not checking return values from
json.Marshalorjson.Unmarshal. - Performance: Parsing tags with
reflecton every request — cache the results. - Over-customization: Too many custom tag keys create opaque behavior — prefer standard tags.
Resources
- Struct Tags Documentation
- JSON Package
- Reflect Package
- Tag Conventions
- Go Encoding and Decoding Data Guide
Summary
Struct tags provide metadata for serialization, validation, and database mapping. Use standard tags like json, db, and xml. Parse tags using reflection when needed. Keep tags consistent and well-documented. Cache tag parsing results for performance.
Comments