JSON 重复键解码时为什么只剩最后一个值?
我第一次遇到这个现象,是在排查一个“配置明明传了两次,程序却只认最后一次”的接口。原始 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 覆盖了。

map 覆盖和嵌套对象合并不是一回事
官方文档把重复键描述为“后值替换或合并先值”,具体表现取决于目标值类型。简单字符串、数字、布尔值最容易观察到覆盖;嵌套对象解到已有的 map 或结构体时,字段级处理可能让结果看起来像合并。
| 目标类型 | 观察到的现象 | 排查重点 |
|---|---|---|
map[string]string | 后一个同名字符串覆盖前一个 | 检查原文顺序和上游拼装逻辑 |
| 结构体字段 | 同一字段后赋值保留 | 确认 json tag 和大小写匹配 |
| 嵌套对象 | 不同子字段可能继续存在 | 不要把“合并”误判为安全去重 |
因此,看到“只剩最后一个值”时,先把目标类型、JSON tag、嵌套层级和重复键的出现顺序一起记下来。只打印 map,无法证明所有目标类型都只会简单覆盖。
进入业务前怎样把重复键变成明确错误
如果 JSON 来自浏览器、合作方或消息队列,重复键可能是上游拼接错误,也可能让不同解析器得出不同业务结果。更稳妥的做法是先保留原文,在语法校验通过后扫描每个对象的成员名;发现同一对象内重复名就拒绝,不把“最后值”交给权限、金额或路由逻辑。
工程上可以把“重复键检测”放在 HTTP 入站层或消息反序列化层,并把对象路径、键名、请求追踪号写入审计日志。日志不要记录完整敏感 payload,只记录足以定位的摘要。

兼容输入与不可信输入应该分开处理
内部旧接口如果已经约定“后值生效”,可以继续使用 v1 默认行为,但应在协议文档里写清楚,并增加重复键测试。外部不可信输入则不建议依赖解析器差异。Go 的 JSON v2 方向已经提供拒绝重复名的选项和错误类型,但是否采用要看项目版本、实验特性和依赖约束;不能因为看到新 API 就直接替换生产解码器。
- 兼容场景:保留原文、记录重复键计数,并让调用方逐步修复。
- 安全场景:重复键直接返回 400 或消息进入隔离队列。
- 审计场景:保存键名与对象路径,不把敏感值写入日志。
我的判断是:只要同一字段会影响权限、金额、状态迁移或下游签名,就把重复键视为输入错误,而不是一种“最后写入获胜”的方便语法。
相关问题
JSON 重复键是合法还是非法?
JSON 标准没有规定重复成员名必须采用哪一种解释,不同实现可以拒绝、覆盖或合并。Go v1 默认接受,协议边界应自行规定。
为什么改成结构体后结果仍然是最后一个值?
因为结构体字段也按输入顺序匹配;同名字段的后一次解码会覆盖先前赋值。需要检查的是原文和字段匹配,不是 map 遍历。
只用 json.Valid 能检测重复键吗?
不能。json.Valid 只判断语法是否有效;重复键在 v1 语法层面可以被接受,需额外做成员名扫描或使用明确拒绝重复名的解码策略。
排查这类问题时,保留原文是第一步,确认目标类型是第二步,最后才决定兼容还是拒绝。这样能把“少了一个值”的现象还原成可解释、可测试的输入规则。
json.Marshal 为什么忽略未导出的字段
- 上一篇
- json.Marshal 为什么忽略未导出的字段
- 下一篇
- time.Parse 解析带中文时区为什么失败
-
- Golang · Go问答 | 16分钟前 |
- bufio.Scanner token too long 如何改用 Reader
- 196浏览 收藏
-
- Golang · Go问答 | 18分钟前 | Go问答 · 数据导入 · Go encoding/csv csv.Reader LazyQuotes ParseError 引号换行
- encoding/csv 遇到引号换行时的解析处理
- 307浏览 收藏
-
- Golang · Go问答 | 26分钟前 | Timer · Go问答 · Go 定时任务 截止时间 time.Ticker time.Timer 周期漂移
- Ticker 周期漂移时如何改成按截止时间调度
- 308浏览 收藏
-
- Golang · Go问答 | 26分钟前 | time · go · time.Parse ParseInLocation Go时区
- time.Parse 解析带中文时区为什么失败
- 222浏览 收藏
-
- Golang · Go问答 | 30分钟前 | 结构体 · api设计 · Go问答 · encoding/json · Go 结构体 dto encoding/json 未导出字段 json.Marshal MarshalJSON
- json.Marshal 为什么忽略未导出的字段
- 295浏览 收藏
-
- Golang · Go问答 | 34分钟前 |
- json.Number 如何避免大整数精度丢失
- 298浏览 收藏
-
- Golang · Go问答 | 35分钟前 | Go问答 · 接口排查 · encoding/json · JSON Go unmarshal encoding/json DisallowUnknownFields UnmarshalTypeError struct tag
- JSON 字段有值却解码为零值的排查清单
- 273浏览 收藏
-
- Golang · Go问答 | 39分钟前 | net/http · Go问答 · Go net/http http/2 ResponseController ResponseWriter Hijack
- ResponseWriter 在 HTTP/2 下为什么不能直接 Hijack
- 115浏览 收藏
-
- Golang · Go问答 | 40分钟前 |
- http.TimeoutHandler 超时后为何还会出现多次写入
- 368浏览 收藏
-
- Golang · Go问答 | 42分钟前 | 重定向 · net/http · HTTP客户端 · Go问答 · Go HTTP重定向 http.Client CheckRedirect 跨主机跳转
- Go http.Client CheckRedirect 处理跨域跳转的配置方法
- 445浏览 收藏
-
- Golang · Go问答 | 1小时前 | net/http · Go问答 · 接口排查 · cookie Go Cookiejar http.Client Set-Cookie AddCookie
- Cookie 设置成功但请求没有携带的原因
- 116浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 1010次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 1014次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 1139次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 1108次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 954次使用
-
- Go netip 怎么做 CIDR 白名单:解析、匹配与失败回归
- 2026-07-24 167浏览
-
- Go unicode/utf8 怎么判断字符串是否含有非法 UTF-8
- 2026-09-07 195浏览
-
- Go json.Decoder UseNumber UseNumber 后类型断言为什么要改成 json.Number
- 2026-09-10 263浏览
-
- Go math/big.Int.SetString 如何判断进制输入失败
- 2026-09-15 334浏览
-
- Go io.LimitReader 读满上限后如何区分截断
- 2026-09-15 232浏览

