8.4 Go Struct Tags 学习笔记
1. Struct Tags 是什么
Struct Tags 是附加在结构体字段后面的元数据,用反引号 ` 包裹,为字段提供额外的配置信息:
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"-"`
Age int `json:"age,omitempty"`
}
Tags 本身不影响代码逻辑,但可以被 encoding/json、反射等包读取和使用。
2. JSON Tag 的三种用法
基本用法 — 重命名字段
Name string `json:"name"`
序列化时,Go 字段名 Name 变成 JSON 的 "name"(小写):
user := User{ID: 1, Name: "John Doe", Email: "john@example.com"}
json.Marshal(user)
// 输出: {"id":1,"name":"John Doe","email":"john@example.com"}
如果不加 tag,JSON 默认使用 Go 的字段名(大写开头):
{"ID":1,"Name":"John Doe","Email":"john@example.com"}
json:"-" — 排除字段
Password string `json:"-"`
该字段不会出现在 JSON 输出中,适合敏感数据(密码、内部ID等):
user := User{ID: 1, Name: "John Doe", Password: "secret123"}
json.Marshal(user)
// 输出: {"id":1,"name":"John Doe","email":"john@example.com"}
// 注意:Password 和 Age(为0) 都没有出现
json:"name,omitempty" — 零值时省略
Age int `json:"age,omitempty"`
当字段值为零值(int为0,string为"",bool为false)时,JSON 输出中省略该字段:
// Age 为 0(零值)
emptyUser := User{ID: 3, Name: "Empty User", Email: "empty@example.com"}
json.Marshal(emptyUser)
// 输出: {"id":3,"name":"Empty User","email":"empty@example.com"}
// 注意:age 字段被省略了!
// Age 为 30(非零值)
fullUser := User{ID: 4, Name: "Full User", Email: "full@example.com", Age: 30}
json.Marshal(fullUser)
// 输出: {"id":4,"name":"Full User","email":"full@example.com","age":30}
// age 字段出现了
3. JSON 反序列化(Unmarshal)
从 JSON 字符串还原为 Go 结构体,JSON tag 同样发挥作用:
jsonData := `{"id":2,"name":"Jane Smith","email":"jane@example.com","age":25}`
var newUser User
json.Unmarshal([]byte(jsonData), &newUser)
// newUser = {ID:2 Name:"Jane Smith" Email:"jane@example.com" Password:"" Age:25}
JSON 中的 "id" 会根据 tag 映射到 Go 的 ID 字段,"name" 映射到 Name 字段。
为什么 Unmarshal 要传指针 &newUser?
Unmarshal 需要修改 newUser 的值 — 如果传值,Go 会传递结构体的拷贝,函数内部修改的是拷贝,外部的
newUser不会变。传指针&newUser让函数直接修改原变量。Unmarshal 需要知道目标类型 — 通过指针,Go 可以推断出要反序列化成
User类型。
对比 Marshal 不需要指针,因为它只读取数据生成 JSON,不修改原值:
json.Marshal(user) // 读取 user,生成 JSON,不需要指针
json.Unmarshal(data, &newUser) // 修改 newUser,必须传指针
这是 Go 的通用规则:函数需要修改传入的变量时,必须传指针。
4. 多种 Tag 类型
一个字段可以有多个 tag,用空格分隔:
type Product struct {
SKU string `json:"sku" validate:"required"`
Name string `json:"product_name" validate:"required,min=3"`
Price float64 `json:"price" validate:"required,min=0"`
}
type Config struct {
DBHost string `json:"db_host" env:"DB_HOST" default:"localhost"`
DBPort int `json:"db_port" env:"DB_PORT" default:"5432"`
}
常见的 tag 类型:
| Tag | 用途 | 示例库 |
|---|---|---|
json |
JSON 序列化/反序列化 |
encoding/json(标准库) |
validate |
数据校验 | github.com/go-playground/validator |
env |
环境变量映射 | github.com/caarlos0/env |
default |
默认值 | 自定义或第三方库 |
db |
数据库映射 | github.com/jmoiron/sqlx |
yaml |
YAML 序列化 | gopkg.in/yaml.v3 |
5. 反射读取 Struct Tags
使用 reflect 包可以在运行时读取 struct tags:
func printStructTags(v interface{}) {
t := reflect.TypeOf(v)
if t.Kind() == reflect.Ptr {
t = t.Elem() // 如果传入指针,取其指向的类型
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// Lookup 返回 tag 值和是否存在的标记
if jsonTag, ok := field.Tag.Lookup("json"); ok {
fmt.Printf(" json: %q\n", jsonTag)
}
if validateTag, ok := field.Tag.Lookup("validate"); ok {
fmt.Printf(" validate: %q\n", validateTag)
}
}
}
执行结果:
Struct tags for User:
ID:
json: "id"
Name:
json: "name"
Email:
json: "email"
Password:
json: "-"
Age:
json: "age,omitempty"
Struct tags for Config:
DBHost:
json: "db_host"
env: "DB_HOST"
default: "localhost"
DBPort:
json: "db_port"
env: "DB_PORT"
default: "5432"
要点:
-
reflect.TypeOf(v)— 获取类型的反射信息 -
t.NumField()— 获取字段数量 -
field.Tag.Lookup("json")— 查找指定 tag,返回(值, 是否存在) - 传入指针时需要用
t.Elem()获取实际类型
知识点总结
| 知识点 | 关键概念 |
|---|---|
| Struct Tags | 反引号包裹的元数据,附加在字段后面 |
| json:"name" | JSON 序列化时重命名字段 |
| json:"-" | 排除字段,不出现在 JSON 中 |
| json:"name,omitempty" | 零值时省略该字段 |
| JSON 反序列化 |
json.Unmarshal 根据 tag 将 JSON 映射回结构体 |
| 多种 Tag | 一个字段可同时有 json、validate、env、default 等多种 tag |
| 反射读取 Tags |
reflect.TypeOf + Tag.Lookup 在运行时读取 tag 信息 |