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"`
}

Struct Tag 的反引号、key-value 语法,以及同一字段被 JSON、ORM、验证库独立读取的关系

核心应用场景

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)

从 reflect.Type 到 StructField 再到 StructTag 的读取链路;对比 Get 与 Lookup 在缺失 key 和显式空值时的语义

omitempty vs omitzero

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

区别

选项判断方式适用场景
omitempty判断是否为 empty:false、数值 0、nil、长度为 0 的集合或字符串等基本类型、字符串、nil 指针
omitzero优先调用 IsZero() bool;没有该方法时判断 Go 零值time.Time、自定义类型

Go 1.24+ 的 encoding/json 中 omitempty 与 omitzero 的精确省略条件,以及 time.Time 和指针时间的不同结果

为什么需要 omitzero

omitempty 不会调用 time.Time.IsZero()。它只判断 empty,而结构体不属于 empty:

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)
// 输出:{"created_at":"0001-01-01T00:00:00Z"}
// time.Time{} 是 struct,不属于 empty,因此不会被 omitempty 省略

如果零时间代表“未设置”,Go 1.24+ 可以改用 omitzero:

1
2
3
4
5
6
7
8
type Event struct {
EndedAt time.Time `json:"ended_at,omitzero"` // 零时间会省略
}

// 如果 API 还要区分“字段未传”与其他状态,使用指针表示可选值
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.Time,二者效果不同:time.Time{} 使用 omitempty 会保留,使用 omitzero 才会因 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 判断 empty,omitzero 按 IsZero() 或 Go 零值判断
  3. 敏感字段用 json:"-" 忽略