
Go 的标准 json.Unmarshal 会静默覆盖重复键,导致数据丢失;本文介绍两种可靠方案——自定义 UnmarshalJSON 类型与手动词法解析,实现在反序列化时主动检测并报错重复键。
go 的标准 `json.unmarshal` 会静默覆盖重复键,导致数据丢失;本文介绍两种可靠方案——自定义 `unmarshaljson` 类型与手动词法解析,实现在反序列化时主动检测并报错重复键。
在 Go 中,JSON 解析器遵循 RFC 7159,不保证键的唯一性,且 encoding/json 包默认行为是“后出现的同名键值覆盖前值”——例如 { "name": "a", "name": "b" } 反序列化后 name 字段值恒为 "b",而错误已被静默吞没。这对配置文件、API 响应等关键场景构成严重隐患。幸运的是,Go 提供了灵活的扩展机制,可主动拦截并校验重复键。
方案一:封装字段类型 + UnmarshalJSON 接口(推荐用于单字段校验)
适用于需对特定字段(如 name)强制唯一性的场景。通过定义一个带状态的字符串类型,记录是否已被赋值:
type singleAssignString string
func (s *singleAssignString) UnmarshalJSON(data []byte) error {
// 去除 JSON 字符串引号并解码
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}
// 若已赋值,拒绝二次写入
if s != nil && *s != "" {
return fmt.Errorf("duplicate key 'name': multiple assignments detected")
}
*s = singleAssignString(str)
return nil
}
// 使用示例结构体
type Gauge struct {
Name singleAssignString `json:"name"`
}
当 JSON 中出现 "name":"a","name":"b" 时,第二次 UnmarshalJSON 调用将返回明确错误,而非静默覆盖。
方案二:手动词法解析(推荐用于全局键校验)
若需检测整个对象内任意键的重复(如验证配置文件合法性),应使用 json.Decoder 逐词元解析,精确捕获键-值对顺序:
type Gauge struct {
Name string
}
func (g *Gauge) UnmarshalJSON(data []byte) error {
dec := json.NewDecoder(bytes.NewReader(data))
// 预期 token 序列:{ → string(key) → : → value → , | }
var (
lastKey string
seenKeys = make(map[string]bool)
inObject = false
)
for {
tok, err := dec.Token()
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("parse error: %w", err)
}
switch v := tok.(type) {
case json.Delim:
if v == '{' {
inObject = true
} else if v == '}' {
inObject = false
return nil
}
case string:
if !inObject {
continue
}
if lastKey == "" {
// 当前 token 是键名
if seenKeys[v] {
return fmt.Errorf("duplicate key %q found", v)
}
seenKeys[v] = true
lastKey = v
} else {
// 当前 token 是上一个键对应的值,重置 lastKey
lastKey = ""
}
}
}
return nil
}
该方法能精准定位重复键位置(如 "name" 出现两次),并返回可读性强的错误信息,适合集成到配置加载、API 入参校验等关键流程中。
注意事项与最佳实践
- ✅ 优先使用方案一:简单、安全、符合 Go 惯例,适用于已知需校验的字段;
- ⚠️ 方案二复杂度高:需手动管理 token 状态机,易出错,建议封装为通用 StrictJSONUnmarshaler 工具函数;
- ❌ 避免依赖第三方库强行“修复”标准库行为——Go 社区共识是显式控制优于隐式修复;
- ? 生产环境建议在 UnmarshalJSON 中添加上下文信息(如文件路径、行号),便于调试;
- ? 对于嵌套结构,需递归应用上述策略,或改用 json.RawMessage 延迟解析。
通过主动校验而非被动接受,你不仅能避免静默数据丢失,更能将 JSON 结构约束转化为可测试、可审计的代码契约。











