Struct Tag 是 Go 语言中一个简洁但极其强大的特性。它允许你为结构体字段附加元数据,这些元数据可以被反射读取,广泛用于序列化、ORM、验证等场景。

基本语法

Struct Tag 是紧跟在字段类型后面的反引号字符串

1
2
3
4
5
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
Email string `json:"email"`
}

语法规则

  1. 使用反引号 ` 包裹,不是双引号
  2. 键值对组成,格式为 key:"value"
  3. 多个 tag 用空格分隔
  4. 值必须用双引号包裹
1
2
3
4
type Product struct {
ID int `json:"id" db:"product_id" validate:"required"`
Name string `json:"name" db:"product_name"`
}

核心应用场景

JSON 序列化

最常用的场景,控制结构体与 JSON 之间的映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Person struct {
// 指定 JSON 字段名
FirstName string `json:"first_name"`

// omitempty: 零值时省略
MiddleName string `json:"middle_name,omitempty"`

// omitzero: 使用 IsZero() 方法判断(Go 1.24+)
DeletedAt time.Time `json:"deleted_at,omitempty"` // 零值会被省略
UpdatedAt time.Time `json:"updated_at,omitzero"` // 通过 IsZero() 判断

// 短横线表示忽略该字段
Password string `json:"-"`

// 空 tag 使用字段名
Age int `json:""`
}

数据库 ORM

GORM 等 ORM 库使用 tag 定义表结构:

1
2
3
4
5
6
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"uniqueIndex;size:50"`
Email string `gorm:"index;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}

数据验证

validator 库通过 tag 定义验证规则:

1
2
3
4
5
6
type RegisterRequest struct {
Username string `validate:"required,min=3,max=20"`
Email string `validate:"required,email"`
Age int `validate:"gte=0,lte=150"`
Password string `validate:"required,min=8"`
}

XML 映射

1
2
3
4
5
type Config struct {
Server string `xml:"server,attr"` // 作为属性
Port int `xml:"port,attr"`
Database string `xml:"database>name"` // 嵌套元素
}

反射读取 Tag

使用 reflect 包可以动态读取 tag:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func parseTags(t interface{}) {
rt := reflect.TypeOf(t)

for i := 0; i < rt.NumField(); i++ {
field := rt.Field(i)

// 获取 json tag
jsonTag := field.Tag.Get("json")
fmt.Printf("Field: %s, JSON Tag: %s\n", field.Name, jsonTag)

// 获取所有 tag
fmt.Printf("All Tags: %s\n", field.Tag)
}
}

type Example struct {
Name string `json:"name" db:"user_name"`
}

parseTags(Example{})
// 输出:
// Field: Name, JSON Tag: name
// All Tags: json:"name" db:"user_name"

Tag 查找方法

1
2
field.Tag.Get("json")        // 返回 "name",找不到返回 ""
field.Tag.Lookup("json") // 返回 ("name", true),找不到返回 ("", false)

omitempty vs omitzero

Go 1.24 引入了 omitzero 选项,解决了 omitempty 的一个痛点。

区别

选项判断方式适用场景
omitempty与零值比较(== zero基本类型、字符串、指针
omitzero调用 IsZero() bool 方法time.Time、自定义类型

问题示例

omitemptytime.Time 的处理基于零值比较:

1
2
3
4
5
6
7
8
type Record struct {
CreatedAt time.Time `json:"created_at,omitempty"`
}

r := Record{CreatedAt: time.Time{}} // 0001-01-01 00:00:00
data, _ := json.Marshal(r)
// 输出:{}
// time.Time{} 是零值,omitempty 会省略

但问题在于非零时间的判断:

1
2
3
4
5
6
7
8
9
10
type Event struct {
EndedAt time.Time `json:"ended_at,omitempty"`
}

// 假设事件确实没有结束时间,但你想显式表示"未设置"
// time.Time 没有"未设置"状态,只有零值和非零值
// 如果你需要一个"可能不存在"的时间,应该用指针
type Event2 struct {
EndedAt *time.Time `json:"ended_at,omitempty"` // 指针 nil 时省略
}

omitzero 解决方案

1
2
3
4
5
6
7
8
type Record struct {
CreatedAt time.Time `json:"created_at,omitzero"`
}

r := Record{CreatedAt: time.Time{}}
data, _ := json.Marshal(r)
// 输出: {}
// omitzero 调用 time.Time.IsZero(),正确识别为零值

对于 time.Timeomitemptyomitzero 效果相同,因为 time.Time{} 既是零值,IsZero() 也返回 true。

omitzero 的真正用武之地是自定义类型:

1
2
3
4
5
6
7
8
9
10
11
12
type NullableInt struct {
value int
valid bool
}

func (n NullableInt) IsZero() bool {
return !n.valid
}

type Data struct {
Count NullableInt `json:"count,omitzero"` // 只有 valid=false 时才省略
}

自定义 IsZero

你也可以为自定义类型实现 IsZero

1
2
3
4
5
6
7
8
9
10
11
12
13
type Optional[T any] struct {
Value T
Valid bool
}

func (o Optional[T]) IsZero() bool {
return !o.Valid
}

type User struct {
Name string `json:"name"`
Phone Optional[string] `json:"phone,omitzero"`
}

最佳实践

命名规范

  • JSON tag 使用 snake_case
  • 保持与 API 文档一致
  • 避免使用缩写,除非非常通用
1
2
3
4
5
6
7
8
9
10
11
// 好的实践
type Good struct {
UserID int64 `json:"user_id"`
CreatedAt string `json:"created_at"`
}

// 避免
type Bad struct {
UserId int64 `json:"userId"` // 不一致
CreatedAt string `json:"createdAt"` // 应该用 snake_case
}

谨慎使用 omitempty

1
2
3
4
5
6
type Data struct {
Count int `json:"count,omitempty"`
}

// 当 Count = 0 时,JSON 中不会有 count 字段
// 这可能导致 API 消费者困惑:是 0 还是没传?

敏感字段处理

1
2
3
4
5
6
7
8
9
10
type User struct {
// 永远不要序列化密码
PasswordHash string `json:"-"`

// 内部 ID 不暴露
InternalID int `json:"-"`

// 外部使用的 ID
PublicID string `json:"id"`
}

多 tag 管理

当 tag 过多时,保持一致的顺序:

1
2
3
4
type Model struct {
// 推荐顺序: json -> db -> validate -> 其他
Name string `json:"name" db:"name" validate:"required"`
}

其他常用 Tag

mapstructure(配置解析)

Viper 等配置库使用:

1
2
3
4
5
6
7
type Config struct {
Database struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
} `mapstructure:"database"`
Debug bool `mapstructure:"debug"`
}

url(表单绑定)

gorilla/schema 等表单解析库使用:

1
2
3
4
5
type SearchParams struct {
Query string `url:"q"`
Page int `url:"page"`
Limit int `url:"limit"`
}

yaml(YAML 序列化)

1
2
3
4
5
6
type Config struct {
Server struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
} `yaml:"server"`
}

inline 内联嵌套

部分库支持将嵌套结构体字段提升到外层:

1
2
3
4
5
6
7
8
9
10
type Base struct {
ID int `json:"id"`
CreatedAt time.Time `json:"created_at"`
}

type User struct {
Base `json:",inline"` // Base 的字段会平铺到 User
Name string `json:"name"`
}
// 输出: {"id":1,"created_at":"...","name":"xxx"}

常见陷阱

格式错误

1
2
3
4
5
6
7
8
9
// 错误:使用双引号包裹
type Wrong struct {
Name string "json:\"name\""
}

// 错误:值没有用双引号
type Wrong2 struct {
Name string `json:name`
}

反射时的指针问题

1
2
3
4
5
6
7
8
func inspect(v interface{}) {
// 如果传入指针,需要先解引用
rt := reflect.TypeOf(v)
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}
// ...
}

未导出字段无法读取

1
2
3
4
type Secret struct {
public string `json:"public"` // 可以读取
private string `json:"private"` // 反射无法访问
}

嵌入字段的 Tag

匿名嵌入字段的 tag 有特殊处理:

1
2
3
4
5
6
7
8
9
10
11
type Base struct {
ID int
}

type User struct {
Base // 继承 Base 的字段
Name string `json:"name"`
}

// 序列化后:{"ID":1,"name":"xxx"}
// 注意:Base 的 ID 字段不会继承 User 的 json 命名空间

string 选项的坑

string 选项只对特定类型有效,且反序列化时类型必须严格匹配:

1
2
3
4
5
6
7
type Data struct {
Count int `json:"count,string"`
}

// 序列化: {"count":"100"}
// 反序列化:json 字符串 "100" 可以解析为 int
// 但 json 数字 100 会报错(必须是字符串形式)

自定义 Tag 解析

你可以实现自己的 tag 解析器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ParseTag 解析 "name,opt1,opt2" 格式的 tag
func ParseTag(tag string) (name string, opts []string) {
parts := strings.Split(tag, ",")
if len(parts) == 0 {
return "", nil
}
return parts[0], parts[1:]
}

// 使用
tag := `json:"user_id,omitempty,string"`
name, opts := ParseTag(tag)
// name = "user_id"
// opts = ["omitempty", "string"]

总结

Struct Tag 体现了 Go 的实用主义哲学:

  • 极简语法:反引号字符串,5 分钟学会
  • 反射驱动:编译期元数据,运行时通过反射读取
  • 生态基石:JSON、ORM、验证库都依赖它

记住三点:

  1. 用反引号,不是双引号
  2. omitempty 比较零值,omitzero 调用 IsZero()
  3. 敏感字段用 json:"-" 忽略