Golang Struct Tag
Struct Tag 是 Go 语言中一个简洁但极其强大的特性。它允许你为结构体字段附加元数据,这些元数据可以被反射读取,广泛用于序列化、ORM、验证等场景。
基本语法
Struct Tag 是紧跟在字段类型后面的反引号字符串:
type User struct {
Name string `json:"name"`
Age int `json:"age,omitempty"`
Email string `json:"email"`
}语法规则
- 使用反引号
`包裹,不是双引号 - 由键值对组成,格式为
key:"value" - 多个 tag 用空格分隔
- 值必须用双引号包裹
type Product struct {
ID int `json:"id" db:"product_id" validate:"required"`
Name string `json:"name" db:"product_name"`
}核心应用场景
1. JSON 序列化
最常用的场景,控制结构体与 JSON 之间的映射:
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:""`
}2. 数据库 ORM
GORM 等 ORM 库使用 tag 定义表结构:
type User struct {
ID uint `gorm:"primaryKey"`
Username string `gorm:"uniqueIndex;size:50"`
Email string `gorm:"index;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
}3. 数据验证
validator 库通过 tag 定义验证规则:
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"`
}4. XML 映射
type Config struct {
Server string `xml:"server,attr"` // 作为属性
Port int `xml:"port,attr"`
Database string `xml:"database>name"` // 嵌套元素
}反射读取 Tag
使用 reflect 包可以动态读取 tag:
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 查找方法
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、自定义类型 |
问题示例
omitempty 对 time.Time 的处理基于零值比较:
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 会省略但问题在于非零时间的判断:
type Event struct {
EndedAt time.Time `json:"ended_at,omitempty"`
}
// 假设事件确实没有结束时间,但你想显式表示"未设置"
// time.Time 没有"未设置"状态,只有零值和非零值
// 如果你需要一个"可能不存在"的时间,应该用指针
type Event2 struct {
EndedAt *time.Time `json:"ended_at,omitempty"` // 指针 nil 时省略
}omitzero 解决方案
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,omitempty 和 omitzero 效果相同,因为 time.Time{} 既是零值,IsZero() 也返回 true。
omitzero 的真正用武之地是自定义类型:
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:
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"`
}最佳实践
1. 命名规范
- JSON tag 使用
snake_case - 保持与 API 文档一致
- 避免使用缩写,除非非常通用
// 好的实践
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
}2. 谨慎使用 omitempty
type Data struct {
Count int `json:"count,omitempty"`
}
// 当 Count = 0 时,JSON 中不会有 count 字段
// 这可能导致 API 消费者困惑:是 0 还是没传?3. 敏感字段处理
type User struct {
// 永远不要序列化密码
PasswordHash string `json:"-"`
// 内部 ID 不暴露
InternalID int `json:"-"`
// 外部使用的 ID
PublicID string `json:"id"`
}4. 多 tag 管理
当 tag 过多时,保持一致的顺序:
type Model struct {
// 推荐顺序: json -> db -> validate -> 其他
Name string `json:"name" db:"name" validate:"required"`
}其他常用 Tag
mapstructure(配置解析)
Viper 等配置库使用:
type Config struct {
Database struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
} `mapstructure:"database"`
Debug bool `mapstructure:"debug"`
}url(表单绑定)
gorilla/schema 等表单解析库使用:
type SearchParams struct {
Query string `url:"q"`
Page int `url:"page"`
Limit int `url:"limit"`
}yaml(YAML 序列化)
type Config struct {
Server struct {
Host string `yaml:"host"`
Port int `yaml:"port"`
} `yaml:"server"`
}inline 内联嵌套
部分库支持将嵌套结构体字段提升到外层:
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. 格式错误
// 错误:使用双引号包裹
type Wrong struct {
Name string "json:\"name\""
}
// 错误:值没有用双引号
type Wrong2 struct {
Name string `json:name`
}2. 反射时的指针问题
func inspect(v interface{}) {
// 如果传入指针,需要先解引用
rt := reflect.TypeOf(v)
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
}
// ...
}3. 未导出字段无法读取
type Secret struct {
public string `json:"public"` // 可以读取
private string `json:"private"` // 反射无法访问
}4. 嵌入字段的 Tag
匿名嵌入字段的 tag 有特殊处理:
type Base struct {
ID int
}
type User struct {
Base // 继承 Base 的字段
Name string `json:"name"`
}
// 序列化后:{"ID":1,"name":"xxx"}
// 注意:Base 的 ID 字段不会继承 User 的 json 命名空间5. string 选项的坑
string 选项只对特定类型有效,且反序列化时类型必须严格匹配:
type Data struct {
Count int `json:"count,string"`
}
// 序列化: {"count":"100"}
// 反序列化:json 字符串 "100" 可以解析为 int
// 但 json 数字 100 会报错(必须是字符串形式)自定义 Tag 解析
你可以实现自己的 tag 解析器:
// 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、验证库都依赖它
记住三点:
- 用反引号,不是双引号
omitempty比较零值,omitzero调用IsZero()- 敏感字段用
json:"-"忽略


