Table Of Contents
- Introduction
- Understanding Struct Tags Basics
- JSON Serialization Tags
- Database Mapping with Tags
- Validation Tags
- Advanced Struct Tag Patterns
- Best Practices
- Common Pitfalls and Solutions
- FAQ
- Conclusion
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!
Add Comment
No comments yet. Be the first to comment!