Go struct 标签
结构体字段上的元数据字符串;encoding/json 等通过反射读取,驱动序列化、校验与 ORM 映射。
[!info] related notes
- 所属 MOC:类型与抽象 · 学习路线
- 前置概念:struct 与方法、反射
- 后续概念:校验、嵌入与组合
- 相关:API 约定、JSON 与序列化
- 实战:User 结构体时间字段、可空时间 *time.Time
Go struct 标签
这个概念为什么会出现
结构体字段在 Go 里首先是内存布局与类型信息。但工程还需要不污染领域类型的元数据:
- JSON 字段名与 omitempty
- DB 列名
- 校验规则
- 配置库的 key
若为每种格式再写一套平行结构体,重复且易漂。struct tag 把约定字符串贴在字段旁,由库在运行时用反射读取。语言要求标签为字符串字面量;key:“value” 是 reflect.StructTag 使用的惯例,不是编译器强制的语义 schema。
[!abstract] 一句话理解 struct tag 是字段声明上的只读字符串元数据;
reflect.StructTag按 key 解析;encoding/json等标准库与生态库据此决定编解码与映射行为。
最小可运行示例
先把示例放进业务场景,再看代码:
场景:用户 API 响应 DTO,密钥绝不能出站
GET /me 返回用户资料 JSON:
- 字段名要是前端约定的
id/name(不是 Go 的ID/Name) - 邮箱为空时不要输出
"email":"" - 内存里有
Token用于服务端会话,序列化时必须丢掉
struct tag 就是贴在字段上的元数据,给 encoding/json 等库通过反射读取。
package main
import (
"encoding/json"
"fmt"
"reflect"
)
// UserDTO:对外 JSON 形状由 tag 描述,而不是字段名本身。
type UserDTO struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // 空则省略
Token string `json:"-"` // 默认 JSON 字段映射忽略;不是全局脱敏机制
}
func main() {
u := UserDTO{ID: 1, Name: "Ada", Token: "secret"}
// Email 为零值 "" → omitempty 生效,JSON 里没有 email
// Token 有值,但 json:"-" 仍被忽略
b, err := json.Marshal(u)
if err != nil {
panic(err)
}
fmt.Println(string(b)) // {"id":1,"name":"Ada"}
// 调试/写通用库时:用反射读出 tag 原文
t := reflect.TypeOf(u)
f, _ := t.FieldByName("Email")
fmt.Println("email tag:", f.Tag.Get("json")) // email,omitempty
}
保存为新模块的 main.go,先 go mod init example.com/tags-demo,再运行:
go run .
期望输出:
{"id":1,"name":"Ada"}
email tag: email,omitempty
结合场景再看三个关注点
- tag 是给库看的元数据,语言本身不解释
json含义。 - json:”-” 让 encoding/json 的默认结构体编解码忽略该字段;自定义 MarshalJSON、其他编码器或日志打印不会自动受它保护。
- omitempty 只影响编码是否省略“空值”,不控制解码字段是否必填;标签通常用反引号,也可用带转义的双引号字符串字面量。
核心概念与准确模型
语法
Field Type `key:"value" other:"x,y"`
- 通常用反引号原始字符串,也可写
Field string "json:\"field\""。 - 多个
key:"value"以空格分隔 - 值里可用逗号表达选项(由具体库解释)
Spec — Tag 规定 tag 是可选字符串字面量;解析惯例见 reflect.StructTag。
反射 API
tag := field.Tag // reflect.StructTag
v := tag.Get("json") // 无则 ""
v, ok := tag.Lookup("json") // 区分缺失与空值
格式不符合惯例时,Get/Lookup 的结果未指定,不能把某次返回空当成可靠诊断;Lookup 的 bool 表示是否找到键,不是格式错误对象。使用 go vet 检查常见标签问题,并验证对应库的实际行为。
标准库:json 标签(最常用)
| 片段 | 含义 |
|---|---|
json:"name" | 字段名 |
json:"-" | 忽略 |
json:",omitempty" | 空值省略(名仍默认) |
json:"name,omitempty" | 改名 + 省略 |
json:",string" | 部分数值等按 JSON 字符串编解码 |
encoding/json 的 omitempty 空值包括 false、数值 0、nil 指针/nil 接口,以及长度为 0 的字符串、数组、切片、map;结构体零值(如 time.Time{})不因此自动省略,非 nil 接口里的 typed nil 也不等于空接口。Go 1.24+ 的 omitzero 则按 IsZero() bool 方法或类型零值判断,可用于零时间;两者同时指定时满足任一个就省略。
json:”-” 与 json:”-,” 不同:后者把字段名设为字面量 -。这些规则针对 encoding/json 默认编解码,自定义方法可改变最终结果。
可见性
普通未导出字段不会因 json tag 而被默认编解码;但嵌入的未导出结构体类型仍可能提升其导出字段。不要把“小写字段忽略”概括为整个嵌入对象必然不可见。
嵌入字段
encoding/json 默认会提升嵌入结构体的字段;嵌入字段有显式 JSON 名时改为嵌套字段。在最浅候选层发生同名冲突时,有标签者优先;若仍有多个候选则全部忽略,不报错。具体示例与完整规则见 go-json-and-serialization。
其它常见 key(生态)
| key | 典型用途 |
|---|---|
xml | encoding/xml |
yaml | yaml 库 |
db / gorm | SQL 映射 |
validate | 校验器 |
mapstructure | map→struct |
form / query | HTTP 绑定 |
不要假设所有 key 语义统一;以对应库文档为准。
性能与设计边界
每次反射读 tag 有成本;热路径库会缓存类型描述。领域核心逻辑不应依赖 tag 才能正确——tag 是适配层配置。
边界情况
- 标签拼写错误:静默使用默认字段名,难发现。
- omitempty 与 false/0:合法零值被省略导致 API 歧义。
- 指针 vs 值:新建目标中的 *T 可区分缺失/null 与非 null 的零值,但缺失和 null 都可能是 nil;重用目标时缺失保留旧值、null 清 nil。三态 PATCH 需额外 presence 信息。
- 冲突字段名:多字段映射同一 JSON 名行为依赖库。
- 自定义类型:需
MarshalJSON/UnmarshalJSON时 tag 不够用。 - 安全:json:”-” 只作用于对应默认 JSON 字段映射;优先输出不含敏感字段的 DTO,并测试实际序列化路径。
常见误区
[!warning] 常见误区:tag 是语言强制模式 错误:以为编译器理解
validate:"required"。
正确:只是字符串;无库读取则无效果。
[!warning] 常见误区:给未导出字段加 json tag 期望生效 错误:
name string \json:“name”“。
正确:字段名首字母大写,或手写编解码。
[!warning] 常见误区:用 tag 堆砌全部业务规则 错误:几十个 validate 规则取代领域校验。
正确:边界用 tag/校验库;核心不变量用代码。
[!warning] 常见误区:手写解析不遵循 StructTag 格式 错误:自创无法用
Get的格式。
正确:遵循key:"value"空格分隔惯例。
工程实践
- API DTO 与领域模型分离,tag 留在 DTO。
- 敏感字段默认
json:"-"或显式 DTO。 - 契约测试:黄金 JSON 文件防 tag 误改。
- 生成:protobuf/openapi 生成 struct 时审 tag。
- 统一 key 字典,避免
json/Json混用。 - 自定义 tag 写清语法、缺失/空值区别和解析失败策略;Lookup 本身不返回 error。
- 与嵌入:明确提升字段的 JSON 形状。
// 可选字段常用指针
type Patch struct {
Name *string `json:"name,omitempty"`
}
这个 Patch 在新建值上不能区分 {} 与 {“name”:null},两者都会得到 nil;omitempty 只决定之后如何编码,不补回输入存在性。
标签还参与结构体类型身份;仅标签不同的结构体不必然可直接赋值,但符合条件的显式结构体转换会忽略标签。不要把 tag 看成对类型系统完全透明的注释。
可验证实验
实验 1:omitempty
比较 Email:"" 与有值时的 JSON。
实验 2:json:"-"
确认默认 Marshal 不输出 Token,Unmarshal 也不填它;自定义 MarshalJSON 的行为需另外测试。
实验 3:反射 Lookup
对缺失 key 与 key:"" 区分 ok。
实验 4:未导出字段
小写字段带 tag,观察是否出现在 JSON。
实验 5:自定义 MarshalJSON
与 tag 共存时谁优先(方法优先于默认 tag 逻辑)。
本节总结
- 本质:字段级字符串元数据 + 反射消费。
- 关键规则:导出字段、库定义语义、
Get/Lookup、json 选项。 - 最易错:拼写静默失败、omitempty 零值、未导出字段、安全泄漏。
- 下一步:反射 原理;校验 实践。
自测题
概念题
- 谁解释
validate:"required"的含义? json:"-"做什么?- 为什么 tag 必须是字符串字面量?
代码推理题
type T struct {
N int `json:"n,omitempty"`
}
json.Marshal(T{})
输出是什么?为何?
工程思考题
同一结构体既要 JSON 对外又要 SQL 扫描,如何组织 tag 与类型?
参考答案
展开
- 校验库在运行时,不是 Go 编译器。
- 编解码时忽略该字段。
- 语法上 tag 是可选 string lit;编译期固定进类型信息。
代码题:{}——N为零值且 omitempty 省略。
工程题:可一结构多 tag;或 DTO/DAO 分离避免 json 与 db 耦合;扫描用sql.Null*或指针表达 NULL。
延伸阅读与资料来源
| 资料 | 类型 | 支撑 |
|---|---|---|
| Spec — Tag | 规范 | 语法 |
| reflect.StructTag | 标准库 | 解析 |
| encoding/json | 标准库 | json tag 语义 |
| encoding/xml | 标准库 | xml tag |
| Effective Go — Structs | 文档 | 结构体风格 |
笔记元信息
- 建议文件名:
go-struct-tags.md - 所属阶段:类型与互操作
- 建议下一篇:Go 反射
- 本篇状态:已深化