登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

JSON 重复键解码时为什么只剩最后一个值?

来源:17golang原创

时间:2026-11-13 09:45:30 439浏览 收藏

我第一次遇到这个现象,是在排查一个“配置明明传了两次,程序却只认最后一次”的接口。原始 JSON 是 {"mode":"safe","mode":"fast"},解码到 Go 的 map[string]string 后只剩 fast。这不是 map 随机丢数据,而是 v1 encoding/json 按 JSON 成员出现顺序处理,重复键的后值覆盖先值。

如果重复键来自外部请求,不要把“最后一个值”当成业务规则。先保留原文并检测重复名;只有明确接受兼容语义时,才让默认解码继续。
要点速览
  • v1 encoding/json 允许重复键,简单字段通常由后值覆盖先值。
  • 结构体和嵌套对象可能出现“覆盖”与“合并”两种结果,不能只看 map 示例。
  • 安全边界上的 JSON 应在进入业务结构前检测重复键,并记录拒绝原因。

最小样例为什么只打印最后一个值

先不要从业务代码猜原因,用同一段输入分别解到 map 和结构体。下面的注释只解释关键断点,运行结果是文章中的示意输出,不是线上截图。

package main

import (
    "encoding/json"
    "fmt"
)

type Config struct {
    Mode string `json:"mode"`
}

func main() {
    raw := []byte(`{"mode":"safe","mode":"fast"}`)

    var values map[string]string
    // Unmarshal 按成员出现顺序写入 map,后一个 mode 会覆盖前一个。
    if err := json.Unmarshal(raw, &values); err != nil {
        panic(err) // 示例直接终止;服务代码应返回可定位的输入错误。
    }

    var cfg Config
    // 结构体字段也按同一顺序匹配,最后一次赋值保留下来。
    if err := json.Unmarshal(raw, &cfg); err != nil {
        panic(err) // 真实接口应把错误和请求追踪号一起记录。
    }

    fmt.Println(values["mode"], cfg.Mode) // 示意输出:fast fast
}

关键点是“按顺序处理”,不是 Go map 的遍历顺序。JSON 对象里的两个 mode 已经在解码阶段落到同一个目标字段,后来的 fast 把先前的 safe 覆盖了。

Go encoding/json 重复键按输入顺序覆盖 map 和结构体字段的静态说明图
图1:重复键顺序说明图,展示同名成员从原始 JSON 进入 map 与结构体后由后值覆盖先值;这是原创静态说明图,不是运行截图。

map 覆盖和嵌套对象合并不是一回事

官方文档把重复键描述为“后值替换或合并先值”,具体表现取决于目标值类型。简单字符串、数字、布尔值最容易观察到覆盖;嵌套对象解到已有的 map 或结构体时,字段级处理可能让结果看起来像合并。

目标类型观察到的现象排查重点
map[string]string后一个同名字符串覆盖前一个检查原文顺序和上游拼装逻辑
结构体字段同一字段后赋值保留确认 json tag 和大小写匹配
嵌套对象不同子字段可能继续存在不要把“合并”误判为安全去重

因此,看到“只剩最后一个值”时,先把目标类型、JSON tag、嵌套层级和重复键的出现顺序一起记下来。只打印 map,无法证明所有目标类型都只会简单覆盖。

进入业务前怎样把重复键变成明确错误

如果 JSON 来自浏览器、合作方或消息队列,重复键可能是上游拼接错误,也可能让不同解析器得出不同业务结果。更稳妥的做法是先保留原文,在语法校验通过后扫描每个对象的成员名;发现同一对象内重复名就拒绝,不把“最后值”交给权限、金额或路由逻辑。

工程上可以把“重复键检测”放在 HTTP 入站层或消息反序列化层,并把对象路径、键名、请求追踪号写入审计日志。日志不要记录完整敏感 payload,只记录足以定位的摘要。

Go JSON 入站层保留原文并检测重复键后再进入业务结构的防护边界说明图
图2:入站防护边界说明图,展示原文保留、重复名检测、审计拒绝和业务解码的关系;这是原创静态说明图,不是运行截图。

兼容输入与不可信输入应该分开处理

内部旧接口如果已经约定“后值生效”,可以继续使用 v1 默认行为,但应在协议文档里写清楚,并增加重复键测试。外部不可信输入则不建议依赖解析器差异。Go 的 JSON v2 方向已经提供拒绝重复名的选项和错误类型,但是否采用要看项目版本、实验特性和依赖约束;不能因为看到新 API 就直接替换生产解码器。

  • 兼容场景:保留原文、记录重复键计数,并让调用方逐步修复。
  • 安全场景:重复键直接返回 400 或消息进入隔离队列。
  • 审计场景:保存键名与对象路径,不把敏感值写入日志。

我的判断是:只要同一字段会影响权限、金额、状态迁移或下游签名,就把重复键视为输入错误,而不是一种“最后写入获胜”的方便语法。

相关问题

JSON 重复键是合法还是非法?

JSON 标准没有规定重复成员名必须采用哪一种解释,不同实现可以拒绝、覆盖或合并。Go v1 默认接受,协议边界应自行规定。

为什么改成结构体后结果仍然是最后一个值?

因为结构体字段也按输入顺序匹配;同名字段的后一次解码会覆盖先前赋值。需要检查的是原文和字段匹配,不是 map 遍历。

只用 json.Valid 能检测重复键吗?

不能。json.Valid 只判断语法是否有效;重复键在 v1 语法层面可以被接受,需额外做成员名扫描或使用明确拒绝重复名的解码策略。

排查这类问题时,保留原文是第一步,确认目标类型是第二步,最后才决定兼容还是拒绝。这样能把“少了一个值”的现象还原成可解释、可测试的输入规则。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>