json 比较不可靠因字段顺序不保证、零值忽略、浮点精度丢失等;应先解码再结构化比对,或用 canonicaljson 统一格式后 diff。

为什么 json.Marshal 直接比字符串不可靠
因为 Go 的 json.Marshal 不保证字段顺序,且会忽略零值字段(除非显式加 omitempty),更别说浮点数精度、NaN、+0/-0 这些边界情况。直接 string(json1) == string(json2) 看似快,但只要结构体字段顺序不同或嵌套 map 无序,就必然误判。
- 使用场景:CI 中校验 API 响应快照、本地调试时比对前后端 JSON 格式一致性
- 常见错误现象:
"{\"a\":1,\"b\":2}" != "{\"b\":2,\"a\":1}"返回 true,实际内容相同却报差异 - 正确做法是先解码为
map[string]interface{}或自定义结构体,再递归比对键值对和类型 - 注意:
json.Unmarshal对 float64 默认精度是 64 位,但若原始 JSON 含1.0000000000000001,Go 会截断为1.0,导致误判——此时需用json.RawMessage+ 字符串 diff 作为 fallback
用 github.com/sergi/go-diff 做结构化 diff 的前提条件
这个库本身不处理 JSON 解析,它只比对文本行或字节流。想让它输出“哪一行字段变了”,得先确保输入是格式化后、字段顺序一致的 JSON 字符串;否则 diff 结果全是“整块重写”,失去定位能力。
- 必须先用
json.MarshalIndent统一缩进(比如四空格),并设置sortKeys: true(Go 1.21+ 可用json.Encoder.SetEscapeHTML(false)配合自定义 encoder,但排序仍需手动) - 推荐封装一个
canonicalJSON函数:先json.Unmarshal→ 再按 key 字典序递归排序 map → 最后json.MarshalIndent - 性能影响:对 >1MB 的 JSON,
Unmarshal+MarshalIndent比纯字符串 diff 慢 3–5 倍,但可读性提升极大;小数据( - 容易踩的坑:
go-diff的DiffStrings默认不忽略空白,换行符差异会被当成真实变更,务必传入diff.LineDiff{IgnoreWhitespace: true}
reflect.DeepEqual 在 JSON 比对中的适用边界
它能跳过序列化过程,直接比内存结构,速度快、语义准,但仅适用于你**完全控制输入结构**的场景——比如比对两个已知结构体变量,或明确知道 JSON 总是解析成 map[string]interface{} 和 []interface{} 的组合。
- 使用场景:单元测试中验证函数返回的 struct 是否与期望 JSON 解析结果一致
- 常见错误现象:把
json.Number("123")和float64(123)当作相等(实际不等),因为json.Unmarshal默认用float64存数字,除非你传UseNumber()选项 - 必须统一解码方式:要么都用
json.Decoder.UseNumber(),要么都转成float64再比(但会丢精度) - 兼容性注意:
nilslice 和空 slice[]int{}在reflect.DeepEqual中视为不同,而某些 JSON 库(如easyjson)可能把缺失字段解析为空 slice,需提前 normalize
如何让差异输出对开发者友好
终端里打印一长串 diff 行没用,关键是要指出「哪个字段路径变了」「旧值/新值是什么」。这需要在递归比对过程中记录路径,而不是依赖最终字符串 diff。
立即学习“go语言免费学习笔记(深入)”;
- 建议用
github.com/mitchellh/mapstructure先转成map[string]interface{},再写一个带path []string参数的递归比对函数 - 示例逻辑:
if v1, ok1 := a[key]; ok1 { if v2, ok2 := b[key]; ok2 { compare(v1, v2, append(path, key)) } else { fmt.Printf("- %s: %v\n", strings.Join(append(path, key), "."), v1) } } - 避免把
time.Time直接塞进 map——JSON 解析后是字符串,比对时类型不一致会全错;统一转成string或int64再进 map - 最容易被忽略的一点:JSON 中的
null解析为 Go 的nil interface{},而结构体字段如果是*string,解码后是(*string)(nil),两者在reflect.DeepEqual中不等,但语义上都代表“空”。要不要等价,得按业务定规则,不能默认










