《Go 程序设计语言》4.5 JSON — 结构体成员 Tag

1. Tag 是什么?

Tag(标签) 是附加在结构体字段上的元信息字符串,用反引号 ` 包裹:

1
2
3
4
5
6
type Movie struct {
Title string
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Actors []string
}

💡 Tag 不影响 Go 代码的逻辑,它是给 encoding/json 等包读取的”标注”,用来控制序列化/反序列化的行为。


2. Tag 的语法

1
`key:"value" key2:"value2"`

以 JSON 为例,通用格式为:

1
`json:"字段名,选项"`
写法 含义
json:"released" JSON 字段名为 released
json:"color,omitempty" 字段名 color零值时省略
json:"-" 完全忽略该字段,不参与 JSON 编解码
json:",omitempty" 字段名用默认(Go 字段名),零值时省略

3. 实际效果演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type Movie struct {
Title string
Year int `json:"released"`
Color bool `json:"color,omitempty"`
Actors []string
}

m := Movie{
Title: "Casablanca",
Year: 1942,
Color: false, // ⚠️ 零值
Actors: []string{"Humphrey Bogart", "Ingrid Bergman"},
}

data, _ := json.MarshalIndent(m, "", " ")
fmt.Println(string(data))

输出结果:

1
2
3
4
5
{
"Title": "Casablanca",
"released": 1942,
"Actors": ["Humphrey Bogart", "Ingrid Bergman"]
}

🔍 关键字段分析

Go 字段 Tag JSON 输出 原因
Title "Title" 无 Tag,直接用 Go 字段名
Year json:"released" "released" Tag 指定了映射名称
Color json:"color,omitempty" ❌ 未出现 值为 false(零值)+ omitempty → 被省略
Actors "Actors" 非零值,正常输出

4. omitempty 的零值规则

omitempty 选项会在字段为零值时省略该字段:

类型 零值 是否被省略
bool false ✅ 是
int / float 0 / 0.0 ✅ 是
string "" ✅ 是
slice / map / pointer nil ✅ 是
struct 所有字段为零值 ✅ 是

⚠️ 注意:空切片 []int{} 不是 nil不会omitempty 省略!

1
2
3
4
5
6
7
8
9
type Data struct {
Items []int `json:"items,omitempty"`
}

// ❌ 不会省略:空但非 nil
d1 := Data{Items: []int{}} // 输出: {"items":[]}

// ✅ 会省略:nil 切片
d2 := Data{Items: nil} // 输出: {}

5. 解码时 Tag 同样生效

1
2
3
4
5
6
jsonStr := `{"released": 1942, "Title": "Casablanca"}`

var m Movie
json.Unmarshal([]byte(jsonStr), &m)

fmt.Println(m.Year) // ✅ 1942 ← JSON 的 "released" 映射到 Go 的 Year 字段

🔹 解码时的匹配规则

  1. 精确匹配 Tag(优先级最高):json:"released" → 匹配 Year
  2. 大小写不敏感匹配字段名"title" → 匹配 Title
  3. 完全匹配字段名"Title" → 匹配 Title

💡 建议始终使用 Tag 明确指定映射关系,避免大小写歧义。


6. 选择性解码

可以定义只包含部分字段的结构体,只解析关心的字段:

1
2
3
4
5
6
7
8
9
// 只关心 Title 和 Year
type MovieTitle struct {
Title string
Year int `json:"released"`
}

var mt MovieTitle
json.Unmarshal(data, &mt)
// ✅ 其他字段(Color, Actors)被自动忽略,不会报错

💡 这是处理大型 JSON 响应时的常用技巧,可提升性能并简化代码。


7. Tag 是通用机制,不只用于 JSON

Tag 是 Go 的通用元数据机制,不同的包读取不同的 key:

1
2
3
4
type User struct {
Name string `json:"name" xml:"name" db:"user_name"`
Email string `json:"email,omitempty" validate:"required,email"`
}
Tag Key 使用者 用途
json encoding/json JSON 序列化/反序列化
xml encoding/xml XML 编解码
db / gorm 数据库 ORM(sqlx、GORM) 映射数据库列名
validate 校验库(go-playground/validator) 字段校验规则
yaml YAML 解析库 YAML 编解码
form Web 框架(Gin、Echo) 表单参数绑定

💡 一个字段可以同时拥有多个 Tag,服务于不同场景。


8. 只有导出字段参与编解码

1
2
3
4
type User struct {
Name string // ✅ 大写开头,会出现在 JSON 中
age int // ❌ 小写开头,json 包看不到,会被忽略
}
字段名 是否导出 是否参与 JSON 编解码
Name ✅ 是 ✅ 是
age ❌ 否 ❌ 否(即使有 Tag 也无效)

⚠️ 重要encoding/json 等标准库只能访问导出字段(大写开头),私有字段会被静默忽略。


🔑 关键要点速记

要点 说明
本质 结构体字段的元信息字符串,用反引号包裹
作用 控制 JSON 字段名映射、零值省略等行为
json:"name" 指定 JSON 字段名
omitempty 零值时不输出该字段
json:"-" 完全忽略该字段
解码匹配 Tag 优先级最高,其次大小写不敏感匹配字段名
导出要求 只有大写开头的字段才参与 JSON 编解码
通用机制 不只用于 JSON,xmldbyamlvalidate 等包都用 Tag

🎯 实践建议

✅ 始终为公共 API 结构体添加 Tag

1
2
3
4
5
6
7
8
9
10
11
// ❌ 不推荐:依赖默认字段名,易受重构影响
type User struct {
UserName string
EmailAddr string
}

// ✅ 推荐:明确指定 JSON 字段名,稳定可靠
type User struct {
UserName string `json:"username"`
EmailAddr string `json:"email"`
}

✅ 合理使用 omitempty 减少冗余

1
2
3
4
5
6
type APIResponse struct {
Code int `json:"code"`
Message string `json:"message,omitempty"` // 可选
Data interface{} `json:"data,omitempty"` // 可选
Error string `json:"error,omitempty"` // 可选
}

✅ 忽略敏感字段

1
2
3
4
5
type User struct {
ID uint `json:"id"`
Name string `json:"name"`
Password string `json:"-"` // ✅ 永不序列化到 JSON
}

✅ 数据库列名映射

1
2
3
4
5
type User struct {
ID uint `json:"id" db:"id"`
Name string `json:"name" db:"user_name"` // Go 字段 vs 数据库列
Email string `json:"email" db:"email_addr"`
}

✅ 校验规则集成

1
2
3
4
5
type RegisterReq struct {
Username string `json:"username" validate:"required,min=3,max=20"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8" json:"-"` // 不输出
}

🔧 调试技巧

查看结构体的实际 Tag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import (
"fmt"
"reflect"
)

func printTags(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)
fmt.Printf("%s: %s\n", field.Name, field.Tag)
}
}

验证 JSON 映射

1
2
3
4
5
6
7
8
9
10
11
12
13
// 快速验证编解码是否符合预期
func testJSON(v interface{}) {
data, err := json.Marshal(v)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))

// 反向验证
var decoded interface{}
json.Unmarshal(data, &decoded)
fmt.Printf("Decoded: %+v\n", decoded)
}

📌 一句话总结:结构体 Tag 是 Go 中连接代码与外部数据格式的”桥梁”,理解其语法、零值规则和导出限制,是编写健壮 API 和数据交互层的关键。