Navigation

Go

Go Struct Tags: Practical Applications and Advanced Patterns (2025)

Master Go struct tags for JSON, database mapping, validation, and custom metadata. Learn advanced patterns and best practices for struct tag usage in Go development.

Table Of Contents

Introduction

Struct tags in Go are one of the language's most powerful yet often underutilized features. These string literals attached to struct fields provide metadata that can be read at runtime using reflection, enabling powerful patterns for serialization, validation, ORM mapping, and much more.

Whether you're building REST APIs, working with databases, or creating configuration systems, understanding struct tags will significantly improve your Go development workflow and code quality. In this comprehensive guide, you'll learn how to leverage struct tags effectively and discover advanced patterns that will make your code more maintainable and robust.

Understanding Struct Tags Basics

What Are Struct Tags?

Struct tags are string literals that follow struct field declarations, providing metadata about how the field should be processed by various packages and libraries.

type User struct {
    ID       int    `json:"id" db:"user_id" validate:"required"`
    Name     string `json:"name" db:"full_name" validate:"required,min=2,max=100"`
    Email    string `json:"email" db:"email_address" validate:"required,email"`
    Age      int    `json:"age,omitempty" db:"age" validate:"min=0,max=150"`
    IsActive bool   `json:"is_active" db:"is_active" validate:"-"`
}

Accessing Struct Tags with Reflection

package main

import (
    "fmt"
    "reflect"
)

type Product struct {
    Name  string `json:"name" xml:"productName" csv:"product_name"`
    Price float64 `json:"price" xml:"cost" csv:"price"`
}

func main() {
    p := Product{}
    t := reflect.TypeOf(p)
    
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("Field: %s\n", field.Name)
        fmt.Printf("  JSON tag: %s\n", field.Tag.Get("json"))
        fmt.Printf("  XML tag: %s\n", field.Tag.Get("xml"))
        fmt.Printf("  CSV tag: %s\n", field.Tag.Get("csv"))
        fmt.Println()
    }
}

JSON Serialization Tags

Basic JSON Tags

The json tag is probably the most commonly used struct tag in Go:

type APIResponse struct {
    Status    string      `json:"status"`
    Message   string      `json:"message,omitempty"`
    Data      interface{} `json:"data,omitempty"`
    Error     *APIError   `json:"error,omitempty"`
    Timestamp int64       `json:"timestamp"`
}

type APIError struct {
    Code    int    `json:"code"`
    Details string `json:"details"`
}

func main() {
    response := APIResponse{
        Status:    "success",
        Data:      map[string]string{"result": "operation completed"},
        Timestamp: time.Now().Unix(),
    }
    
    jsonData, _ := json.Marshal(response)
    fmt.Println(string(jsonData))
    // Output: {"status":"success","data":{"result":"operation completed"},"timestamp":1672531200}
}

Advanced JSON Tag Options

type UserProfile struct {
    // Rename field in JSON
    UserID int `json:"user_id"`
    
    // Omit if empty
    Bio string `json:"bio,omitempty"`
    
    // Always omit from JSON (private field)
    Password string `json:"-"`
    
    // Include even if empty (default behavior)
    LastLogin *time.Time `json:"last_login"`
    
    // Custom field that appears in JSON but not in struct
    DisplayName string `json:"display_name,omitempty"`
    
    // Embed timestamp as string
    CreatedAt time.Time `json:"created_at"`
    
    // Embedded struct with custom naming
    Address `json:"address"`
}

type Address struct {
    Street  string `json:"street"`
    City    string `json:"city"`
    Country string `json:"country"`
}

Custom JSON Marshaling with Tags

type Money struct {
    Amount   int64  `json:"-"`
    Currency string `json:"currency"`
}

func (m Money) MarshalJSON() ([]byte, error) {
    type Alias Money
    return json.Marshal(&struct {
        Amount string `json:"amount"`
        *Alias
    }{
        Amount: fmt.Sprintf("%.2f", float64(m.Amount)/100),
        Alias:  (*Alias)(&m),
    })
}

func (m *Money) UnmarshalJSON(data []byte) error {
    type Alias Money
    aux := &struct {
        Amount string `json:"amount"`
        *Alias
    }{
        Alias: (*Alias)(m),
    }
    
    if err := json.Unmarshal(data, &aux); err != nil {
        return err
    }
    
    amount, err := strconv.ParseFloat(aux.Amount, 64)
    if err != nil {
        return err
    }
    
    m.Amount = int64(amount * 100)
    return nil
}

Database Mapping with Tags

GORM Tags

type User struct {
    ID        uint      `gorm:"primaryKey;autoIncrement"`
    Username  string    `gorm:"uniqueIndex;not null;size:50"`
    Email     string    `gorm:"uniqueIndex;not null;size:100"`
    Password  string    `gorm:"not null;size:255"`
    Age       int       `gorm:"check:age > 0"`
    CreatedAt time.Time `gorm:"autoCreateTime"`
    UpdatedAt time.Time `gorm:"autoUpdateTime"`
    DeletedAt *time.Time `gorm:"index"`
    
    // Relationships
    Profile   Profile   `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"`
    ProfileID uint      `gorm:"index"`
    
    Posts     []Post    `gorm:"foreignKey:AuthorID"`
}

type Profile struct {
    ID       uint   `gorm:"primaryKey"`
    Bio      string `gorm:"type:text"`
    Avatar   string `gorm:"size:255"`
    Website  string `gorm:"size:255"`
}

type Post struct {
    ID       uint   `gorm:"primaryKey"`
    Title    string `gorm:"not null;size:200"`
    Content  string `gorm:"type:text"`
    AuthorID uint   `gorm:"not null;index"`
}

Custom Database Tags

type DBConfig struct {
    tableName string
    fields    map[string]string
}

func parseDBTags(v interface{}) DBConfig {
    t := reflect.TypeOf(v)
    if t.Kind() == reflect.Ptr {
        t = t.Elem()
    }
    
    config := DBConfig{
        fields: make(map[string]string),
    }
    
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        
        if tableName := field.Tag.Get("table"); tableName != "" {
            config.tableName = tableName
        }
        
        if dbField := field.Tag.Get("db"); dbField != "" {
            config.fields[field.Name] = dbField
        }
    }
    
    return config
}

type Product struct {
    _         struct{} `table:"products"`
    ID        int      `db:"product_id"`
    Name      string   `db:"product_name"`
    Price     float64  `db:"unit_price"`
    InStock   bool     `db:"is_available"`
}

Validation Tags

Using Validator Package

import "github.com/go-playground/validator/v10"

type CreateUserRequest struct {
    Username string `json:"username" validate:"required,min=3,max=20,alphanum"`
    Email    string `json:"email" validate:"required,email"`
    Password string `json:"password" validate:"required,min=8,containsany=!@#$%^&*"`
    Age      int    `json:"age" validate:"gte=18,lte=100"`
    Gender   string `json:"gender" validate:"omitempty,oneof=male female other"`
    Website  string `json:"website" validate:"omitempty,url"`
    Phone    string `json:"phone" validate:"omitempty,e164"`
}

func validateStruct(s interface{}) error {
    validate := validator.New()
    return validate.Struct(s)
}

func main() {
    user := CreateUserRequest{
        Username: "john_doe",
        Email:    "john@example.com",
        Password: "password123!",
        Age:      25,
        Website:  "https://johndoe.com",
    }
    
    if err := validateStruct(user); err != nil {
        for _, err := range err.(validator.ValidationErrors) {
            fmt.Printf("Field: %s, Error: %s\n", err.Field(), err.Tag())
        }
    }
}

Custom Validation Tags

func ValidatePasswordComplexity(fl validator.FieldLevel) bool {
    password := fl.Field().String()
    
    hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
    hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
    hasNumber := regexp.MustCompile(`\d`).MatchString(password)
    hasSpecial := regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]`).MatchString(password)
    
    return hasUpper && hasLower && hasNumber && hasSpecial
}

type UserRegistration struct {
    Password string `validate:"required,min=8,password_complexity"`
}

func main() {
    validate := validator.New()
    validate.RegisterValidation("password_complexity", ValidatePasswordComplexity)
    
    user := UserRegistration{Password: "SimplePass"}
    err := validate.Struct(user)
    // Will fail validation
}

Advanced Struct Tag Patterns

Multi-Format Serialization

type Event struct {
    ID          int       `json:"id" xml:"ID" yaml:"id" toml:"id"`
    Name        string    `json:"name" xml:"Name" yaml:"name" toml:"name"`
    Description string    `json:"description,omitempty" xml:"Description,omitempty" yaml:"description,omitempty" toml:"description,omitempty"`
    StartTime   time.Time `json:"start_time" xml:"StartTime" yaml:"start_time" toml:"start_time"`
    EndTime     time.Time `json:"end_time" xml:"EndTime" yaml:"end_time" toml:"end_time"`
    Location    string    `json:"location" xml:"Location" yaml:"location" toml:"location"`
}

// Serialize to different formats
func (e Event) ToJSON() ([]byte, error) {
    return json.Marshal(e)
}

func (e Event) ToXML() ([]byte, error) {
    return xml.Marshal(e)
}

func (e Event) ToYAML() ([]byte, error) {
    return yaml.Marshal(e)
}

Configuration Loading

type AppConfig struct {
    Server   ServerConfig   `mapstructure:"server" yaml:"server" json:"server"`
    Database DatabaseConfig `mapstructure:"database" yaml:"database" json:"database"`
    Redis    RedisConfig    `mapstructure:"redis" yaml:"redis" json:"redis"`
    Logging  LoggingConfig  `mapstructure:"logging" yaml:"logging" json:"logging"`
}

type ServerConfig struct {
    Host         string        `mapstructure:"host" yaml:"host" json:"host" default:"localhost"`
    Port         int           `mapstructure:"port" yaml:"port" json:"port" default:"8080"`
    ReadTimeout  time.Duration `mapstructure:"read_timeout" yaml:"read_timeout" json:"read_timeout" default:"30s"`
    WriteTimeout time.Duration `mapstructure:"write_timeout" yaml:"write_timeout" json:"write_timeout" default:"30s"`
}

type DatabaseConfig struct {
    URL             string `mapstructure:"url" yaml:"url" json:"url" required:"true"`
    MaxOpenConns    int    `mapstructure:"max_open_conns" yaml:"max_open_conns" json:"max_open_conns" default:"25"`
    MaxIdleConns    int    `mapstructure:"max_idle_conns" yaml:"max_idle_conns" json:"max_idle_conns" default:"5"`
    ConnMaxLifetime string `mapstructure:"conn_max_lifetime" yaml:"conn_max_lifetime" json:"conn_max_lifetime" default:"1h"`
}

API Documentation Generation

type User struct {
    ID       int    `json:"id" example:"1" description:"Unique user identifier"`
    Username string `json:"username" example:"john_doe" description:"User's chosen username" minLength:"3" maxLength:"20"`
    Email    string `json:"email" example:"john@example.com" description:"User's email address" format:"email"`
    Age      int    `json:"age" example:"25" description:"User's age in years" minimum:"18" maximum:"100"`
    IsActive bool   `json:"is_active" example:"true" description:"Whether the user account is active"`
}

// Generate OpenAPI schema from tags
func generateOpenAPISchema(t reflect.Type) map[string]interface{} {
    schema := map[string]interface{}{
        "type":       "object",
        "properties": make(map[string]interface{}),
    }
    
    properties := schema["properties"].(map[string]interface{})
    
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        jsonTag := field.Tag.Get("json")
        if jsonTag == "-" {
            continue
        }
        
        fieldName := strings.Split(jsonTag, ",")[0]
        if fieldName == "" {
            fieldName = field.Name
        }
        
        prop := map[string]interface{}{}
        
        if desc := field.Tag.Get("description"); desc != "" {
            prop["description"] = desc
        }
        
        if example := field.Tag.Get("example"); example != "" {
            prop["example"] = example
        }
        
        if format := field.Tag.Get("format"); format != "" {
            prop["format"] = format
        }
        
        properties[fieldName] = prop
    }
    
    return schema
}

Best Practices

1. Use Consistent Tag Naming

// Good: Consistent snake_case for JSON, camelCase for XML
type User struct {
    FirstName string `json:"first_name" xml:"firstName"`
    LastName  string `json:"last_name" xml:"lastName"`
}

// Bad: Inconsistent naming
type User struct {
    FirstName string `json:"firstName" xml:"first_name"`
    LastName  string `json:"last_name" xml:"LastName"`
}

2. Document Custom Tags

// Package mytags provides custom struct tag functionality.
//
// Supported tags:
//   - cache: Controls caching behavior (values: "skip", "ttl:duration")
//   - audit: Enables field auditing (values: "log", "track")
//   - encrypt: Marks field for encryption (values: "aes", "rsa")
//
// Example:
//   type User struct {
//       Password string `encrypt:"aes" audit:"log"`
//       Email    string `cache:"ttl:1h" audit:"track"`
//   }
package mytags

3. Validate Tag Values

func validateTags(t reflect.Type) error {
    validCacheValues := map[string]bool{
        "skip": true,
    }
    
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        
        if cache := field.Tag.Get("cache"); cache != "" {
            if strings.HasPrefix(cache, "ttl:") {
                // Validate TTL format
                ttlStr := strings.TrimPrefix(cache, "ttl:")
                if _, err := time.ParseDuration(ttlStr); err != nil {
                    return fmt.Errorf("invalid ttl duration for field %s: %s", field.Name, ttlStr)
                }
            } else if !validCacheValues[cache] {
                return fmt.Errorf("invalid cache value for field %s: %s", field.Name, cache)
            }
        }
    }
    
    return nil
}

Common Pitfalls and Solutions

1. Tag Syntax Errors

// Wrong: Missing backticks
type User struct {
    Name string json:"name"  // Error!
}

// Wrong: Wrong quote type
type User struct {
    Name string `json:'name'`  // Error!
}

// Correct
type User struct {
    Name string `json:"name"`
}

2. Performance Considerations

// Cache reflection results to avoid repeated type analysis
var tagCache = make(map[reflect.Type]map[string]string)
var tagCacheMu sync.RWMutex

func getFieldTags(t reflect.Type, tagName string) map[string]string {
    tagCacheMu.RLock()
    if cache, exists := tagCache[t]; exists {
        tagCacheMu.RUnlock()
        return cache
    }
    tagCacheMu.RUnlock()
    
    tagCacheMu.Lock()
    defer tagCacheMu.Unlock()
    
    // Double-check after acquiring write lock
    if cache, exists := tagCache[t]; exists {
        return cache
    }
    
    tags := make(map[string]string)
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        if tag := field.Tag.Get(tagName); tag != "" {
            tags[field.Name] = tag
        }
    }
    
    tagCache[t] = tags
    return tags
}

FAQ

Q: Can I use multiple values in a single tag? A: Yes, separate multiple values with commas: json:"name,omitempty" validate:"required,min=3"

Q: How do I ignore fields in JSON but include them in database mapping? A: Use json:"-" to exclude from JSON while keeping other tags: json:"-" db:"secret_field"

Q: Can I access parent struct tags from embedded fields? A: No, embedded fields only have access to their own tags. Use composition instead if you need shared metadata.

Q: Are struct tags type-safe? A: No, struct tags are strings and parsed at runtime. Always validate tag values and handle errors gracefully.

Q: How do I handle tag conflicts between different packages? A: Use different tag names for different purposes: json:"name" xml:"title" db:"full_name"

Q: Can I modify struct tags at runtime? A: No, struct tags are read-only. You need to create new types or use interfaces for dynamic behavior.

Conclusion

Struct tags are a powerful feature that enables clean separation of concerns in Go applications. By mastering their usage patterns, you can create more maintainable and flexible code that works seamlessly with various libraries and frameworks.

Key takeaways:

  • Use struct tags for metadata-driven programming
  • Implement consistent naming conventions
  • Cache reflection results for performance
  • Validate tag values to prevent runtime errors
  • Document custom tag formats clearly
  • Consider using code generation for complex tag scenarios

Start leveraging struct tags in your Go projects to build more robust and maintainable applications. Share your own struct tag patterns and use cases in the comments below!

Share this article

Add Comment

No comments yet. Be the first to comment!

More from Go