← [ BACK_TO_NOTES ]

Go struct 标签

结构体字段上的元数据字符串;encoding/json 等通过反射读取,驱动序列化、校验与 ORM 映射。

#concept #growing #dev #go

[!info] related notes

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

结合场景再看三个关注点

  1. tag 是给库看的元数据,语言本身不解释 json 含义。
  2. json:”-” 让 encoding/json 的默认结构体编解码忽略该字段;自定义 MarshalJSON、其他编码器或日志打印不会自动受它保护。
  3. 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 标签(最常用)

文档:encoding/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典型用途
xmlencoding/xml
yamlyaml 库
db / gormSQL 映射
validate校验器
mapstructuremap→struct
form / queryHTTP 绑定

不要假设所有 key 语义统一;以对应库文档为准。

性能与设计边界

每次反射读 tag 有成本;热路径库会缓存类型描述。领域核心逻辑不应依赖 tag 才能正确——tag 是适配层配置。

边界情况

  1. 标签拼写错误:静默使用默认字段名,难发现。
  2. omitempty 与 false/0:合法零值被省略导致 API 歧义。
  3. 指针 vs 值:新建目标中的 *T 可区分缺失/null 与非 null 的零值,但缺失和 null 都可能是 nil;重用目标时缺失保留旧值、null 清 nil。三态 PATCH 需额外 presence 信息。
  4. 冲突字段名:多字段映射同一 JSON 名行为依赖库。
  5. 自定义类型:需 MarshalJSON/UnmarshalJSON 时 tag 不够用。
  6. 安全:json:”-” 只作用于对应默认 JSON 字段映射;优先输出不含敏感字段的 DTO,并测试实际序列化路径。

常见误区

[!warning] 常见误区:tag 是语言强制模式 错误:以为编译器理解 validate:"required"。
正确:只是字符串;无库读取则无效果。

[!warning] 常见误区:给未导出字段加 json tag 期望生效 错误:name string \json:“name”“。
正确:字段名首字母大写,或手写编解码。

[!warning] 常见误区:用 tag 堆砌全部业务规则 错误:几十个 validate 规则取代领域校验。
正确:边界用 tag/校验库;核心不变量用代码。

[!warning] 常见误区:手写解析不遵循 StructTag 格式 错误:自创无法用 Get 的格式。
正确:遵循 key:"value" 空格分隔惯例。

工程实践

  1. API DTO 与领域模型分离,tag 留在 DTO。
  2. 敏感字段默认 json:"-" 或显式 DTO。
  3. 契约测试:黄金 JSON 文件防 tag 误改。
  4. 生成:protobuf/openapi 生成 struct 时审 tag。
  5. 统一 key 字典,避免 json/Json 混用。
  6. 自定义 tag 写清语法、缺失/空值区别和解析失败策略;Lookup 本身不返回 error。
  7. 与嵌入:明确提升字段的 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 零值、未导出字段、安全泄漏。
  • 下一步:反射 原理;校验 实践。

自测题

概念题

  1. 谁解释 validate:"required" 的含义?
  2. json:"-" 做什么?
  3. 为什么 tag 必须是字符串字面量?

代码推理题

type T struct {
	N int `json:"n,omitempty"`
}
json.Marshal(T{})

输出是什么?为何?

工程思考题

同一结构体既要 JSON 对外又要 SQL 扫描,如何组织 tag 与类型?

参考答案

展开
  1. 校验库在运行时,不是 Go 编译器。
  2. 编解码时忽略该字段。
  3. 语法上 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 反射
  • 本篇状态:已深化
创建于 2026/6/25 更新于 2026/10/7
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本