JSON序列化实际别名由struct tag中的json字段决定,与结构体字段名无关;json.Marshal/Unmarshal完全忽略字段名,只解析json tag,反射修改字段名或tag均非标准做法且存在风险。

struct tag 里的 json 字段才是 JSON 序列化的实际别名
Go 的 json.Marshal 和 json.Unmarshal 完全不看结构体字段名,只认 struct tag 中的 json 键。反射改字段名(比如用 reflect.StructField.Name)对 JSON 输出零影响——因为那只是运行时的元信息,encoding/json 包压根不读它。
常见错误现象:reflect.ValueOf(&s).Elem().FieldByName("ID").SetString("abc") 能改值,但 json.Marshal(s) 还是按原 tag 输出,和字段名是否被“反射重命名”毫无关系。
-
json:tag 值为空(如json:"")会跳过该字段 -
json:"id,string"这种带选项的写法会被正确解析,反射无法模拟这种语义 - 如果字段未导出(小写开头),即使加了
jsontag,json.Marshal也序列化不出——反射也不能让它变导出
想动态控制 JSON key,得在运行时构造 map 或用自定义 MarshalJSON
硬要绕过静态 tag 实现“动态别名”,只有两个靠谱路径:一是不用 struct 直接拼 map[string]interface{};二是为结构体实现 MarshalJSON 方法,在里面手动控制键名。
使用场景:API 响应字段需按请求参数切换(如 ?format=camel),或兼容多个外部系统不同字段约定。
立即学习“go语言免费学习笔记(深入)”;
- 用
map方式最灵活,但丢失类型安全和结构体语义,适合简单扁平数据 - 实现
MarshalJSON时,必须调用json.Marshal处理子字段,不能直接拼字符串,否则会漏转义、破坏嵌套结构 - 注意:如果结构体嵌套了其他自定义
MarshalJSON类型,你的实现里要显式调用它们的MarshalJSON,否则会退化成默认行为
反射改不了字段名,但能读取/修改 struct tag —— 仅限于你 own 这个 struct
你可以用 reflect.TypeOf(T{}).Field(i).Tag.Get("json") 拿到当前 tag 值,甚至用 unsafe + reflect 黑科技强行覆写(仅测试环境可行,生产禁用)。但这不是标准做法,且对已编译的 struct 类型无效——tag 是编译期绑定的只读元数据。
性能与兼容性影响:unsafe 修改 tag 在 Go 1.20+ 有更高概率 panic,且跨平台行为不一致;标准库所有 JSON 相关逻辑都假设 tag 不可变。
- 第三方库如
mapstructure或copier依赖 tag,你改了它们可能失效 - IDE 和静态分析工具(如
go vet)看不到运行时修改后的 tag,提示会错乱 - 真正需要“多套别名”的项目,建议用代码生成(
go:generate+stringer风格模板)而非运行时 hack
别名冲突时,json tag 优先级高于字段名,且大小写敏感
当两个字段 tag 写成一样的 key(如都写 json:"id"),json.Unmarshal 会把值塞进最后一个匹配的字段,而 json.Marshal 只输出一次——这容易引发静默数据覆盖。
容易踩的坑:json:"Id" 和 json:"id" 是两个不同 key,但前端 JS 习惯忽略大小写,导致反序列化后字段“消失”。另外,json:"id,omitempty" 和 json:"id" 并存时,前者不会触发后者的覆盖逻辑。
- 用
go vet -tags可检测重复 tag(需 Go 1.21+) - 如果字段是嵌套 struct,外层
jsontag 不会影响内层字段的 key,除非内层也显式声明 - 空字符串
json:""和省略 tag 效果不同:前者明确排除,后者仍可能被序列化(取决于字段是否导出)










