ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Go 夜读讨论实录:json.Unmarshal 后 interface{} 无法断言为结构体,用 json.RawMessage 延迟解析

Go 夜读讨论实录:json.Unmarshal 后 interface{} 无法断言为结构体,用 json.RawMessage 延迟解析 文档教程【免费下载链接】nightWeekly Go Online Meetup via BilibiliGo 夜读通过 bilibili 在线直播的方式分享 Go 相关的技术话题每天大家在微信/telegram/Slack 上及时沟通交流编程技术话题。项目地址https://gitcode.com/gh_mirrors/ni/night点击查看免费下载本文来自「Go 夜读」微信群 2018-12-04 的技术讨论围绕encoding/json解析复杂 JSON 时的一个高频坑把 JSON 反序列化进interface{}之后再想断言成具体的结构体类型会直接 panic。文章完整保留了群内的原始案例与两种解决方案并结合仓库内的真实配置解析代码深入讲解断言失败的根本原因、json.RawMessage延迟解析的原理以及在配置加载场景中的实战用法。读完你不仅能修掉这个 bug还能掌握处理「结构多变、字段类型不确定」的 JSON 数据的一整套思路。问题复现对[]interface{}直接断言[]Employee报错先看微信群讨论中给出的原始代码。需求很常见一个 JSON 消息里既有普通字段又有类型不确定的params数组数组内可能是字符串也可能是对象数组于是用interface{}作为切片元素类型package main import ( encoding/json ) type Msg struct { Id int json:id Params []interface{} json:params } type Employee struct { Name string json:name } func main() { b : []byte({id:111,params:[boss,[{name:a},{name:b}]]}) msg : Msg{} json.Unmarshal(b, msg) _ msg.Params[0].(string) _ msg.Params[1].([]Employee) // 无法直接断言? }运行结果msg.Params[0].(string)成功但msg.Params[1].([]Employee)直接 panic报错信息形如panic: interface conversion: interface {} is []interface {}, not []main.Employee对于结构复杂多变的数据开发者通常习惯用interface{}兜底接收再按需断言。但上面的例子说明json.Unmarshal之后无法再断言成具体的结构体类型。虽然结构体和map[string]interface{}之间可以互相转换但interface{}里实际装的东西并不是你以为的那个类型。根因分析encoding/json到底往interface{}里塞了什么要理解为什么断言失败关键在于明白encoding/json对interface{}的默认解码规则。当json.Unmarshal遇到的目标值是interface{}或切片元素为interface{}时标准库会按 JSON 的原始类型做一一映射JSON 对象 →map[string]interface{}JSON 数组 →[]interface{}JSON 字符串 →stringJSON 数字 →float64JSON 布尔值 →boolJSON null →nil也就是说上面示例里params数组反序列化后的真实形态是[]interface{}{ boss, // string []interface{}{ // 注意内层是 []interface{} map[string]interface{}{name:a}, map[string]interface{}{name:b}, }, }msg.Params[1]的动态类型是[]interface{}而代码却用.([]Employee)去断言类型不匹配自然触发 panic。map[string]interface{}可以通过类型转换或逐字段取值拿到数据但这要求你了解 JSON 内部的嵌套形态而且拿到的还是interface{}想还原成强类型的[]Employee并没有捷径——json 包不会、也无法替你猜出结构体类型。这一点在本仓库的配置解析代码里也能印证标准用法永远是先定义好带jsontag 的结构体再让json.Unmarshal把数据灌进去。例如 actions/monthly.go 中读取issuesinfo.json配置时就定义了Issue结构体逐个字段声明json:title、json:body等 tag随后才执行json.Unmarshal(data, issueInfo)。2019-07-17 全局变量初始化讨论中读取enode.json配置也同样如此先定义带json:mongodbUri、json:blockChainDatabaseEngine等 tag 的结构体再json.Unmarshal(content, config)。可见「结构体 tag Unmarshal」是类型安全的前提一旦走到interface{}这条岔路类型信息就丢了。解决方案 1️⃣非最优重新 Marshal 再 Unmarshal群里给出的第一种思路把想断言成具体结构体的字段先json.Marshal成原始字节再json.Unmarshal进目标类型。newB, _ : json.Marshal(msg.Params[1]) var employee []Employee json.Unmarshal(newB, employee)思路很直观json.Marshal会把msg.Params[1]此时是[]interface{}map[string]interface{}的嵌套结构重新序列化成 JSON 字节流而 JSON 字节流是不携带类型信息的纯文本json.Unmarshal拿到字节流后按[]Employee的声明去解码类型自然就对上了。代价也很明显多一次序列化 反序列化凭空多出两次内存拷贝和 JSON 文本处理在大数据量或高频路径上性能不可接受出错点增加json.Marshal的错误被忽略了上面代码用_吞掉json.Unmarshal的错误也没处理一旦中间环节出错结果不可预期代码啰嗦每需要解析一个字段就要重复写一遍 Marshal/Unmarshal。所以群里的结论是能用但不推荐。解决方案 2️⃣推荐用json.RawMessage延迟解析第二种方案更优雅把字段类型从interface{}换成json.RawMessage让该字段的解析被延迟到真正使用的时候。package main import ( encoding/json log ) type Msg struct { Id int json:id Params []json.RawMessage json:params } type Employee struct { Name string json:name } func main() { b : []byte({id:111,params:[boss,[{name:a},{name:b}]]}) msg : Msg{} json.Unmarshal(b, msg) var boss string json.Unmarshal(msg.Params[0], boss) log.Println(string(boss)) var employee []Employee json.Unmarshal(msg.Params[1], employee) log.Println(employee) }运行输出boss [{a} {b}]json.RawMessage为什么能解决问题json.RawMessage的本质定义是type RawMessage []byte它是对[]byte的类型别名。关键在于encoding/json对它的特殊对待json.Unmarshal遇到RawMessage类型的字段时不会去解析其中的内容而是把该字段的原始 JSON 字节原封不动地保存下来。因此Msg结构体解析时Params里每一项都只是「暂存的 JSON 片段」没有被提前解码成interface{}类型信息零丢失需要哪个字段就在哪个时间点对对应的RawMessage单独调用json.Unmarshal目标类型可以是string、[]Employee、任何结构体——完全由你掌控由于RawMessage实现了json.Marshaler与json.Unmarshaler接口它既能作为反序列化的「延迟占位符」也能在序列化时原样输出天然适合「先接收、后加工、再透传」的中间层场景。对比方案 1RawMessage方案不经过任何一次中间 Marshal解析只发生一次延迟的那次性能更优语义也更清晰是处理多变结构体 JSON 的首选。使用要点与注意事项错误处理不要省略示例中为了简洁直接忽略了json.Unmarshal的错误生产代码里应逐层检查返回值。RawMessage里的内容可能不是合法 JSON或与目标类型不匹配比如把对象反序列化进string务必处理 error。延迟不等于免检字段什么时候解析由你决定如果一直不解析问题不会被发现所以建议在入口处做一次校验或在用到的路径上严格处理错误。结合自定义UnmarshalJSON如果某个结构体在多处被复用可以把「二次解析」逻辑收敛进该结构体的UnmarshalJSON方法里让调用方无感。这与RawMessage并不冲突前者负责推迟、后者负责接管细节。仓库实战JSON 配置解析场景中的应用json.RawMessage的核心价值在于「先整体接收再按需二次解析」这在配置加载、消息网关、通用接口转发等场景中尤为常见。本仓库虽未直接使用RawMessage但多处代码验证了「结构体 json.Unmarshal」这一基础模式的正确姿势可作为对照actions/monthly.go#L15-L38读取 GitHub Issue 配置 JSON用带jsontag 的Issue结构体接收后直接使用——因为字段类型确定不需要RawMessagezap 日志库封装把日志配置拼成 JSON 字符串后json.Unmarshal进zap.Config结构体——同样的强类型接收模式2019-07-17 全局变量初始化讨论读取enode.json配置结构体验证了配置文件中字段与结构体 tag 的一一对应关系。对照这些代码可以得出结论凡是字段类型在编译期确定就老老实实用具体结构体接收凡是字段类型要到运行时才能确定、或同一字段在不同消息里形态不同才值得动用interface{} 断言或json.RawMessage延迟解析。小结方案做法优点缺点推荐度方案 1重新json.Marshaljson.Unmarshal思路简单、容易理解多一次序列化开销错误点增多代码啰嗦⭐ 不推荐方案 2字段类型改为json.RawMessage按需二次解析无中间序列化类型信息零丢失目标类型完全可控需要手动管理每个字段的解析时机与错误⭐⭐⭐ 推荐一句话总结这次「Go 夜读」微信群的讨论成果json.Unmarshal后的interface{}装的是map[string]interface{}/[]interface{}不是你的结构体直接断言必然 panic处理复杂多变的 JSON用json.RawMessage延迟解析把类型还原的主动权拿回自己手里。赞分享文档教程【免费下载链接】nightWeekly Go Online Meetup via BilibiliGo 夜读通过 bilibili 在线直播的方式分享 Go 相关的技术话题每天大家在微信/telegram/Slack 上及时沟通交流编程技术话题。项目地址https://gitcode.com/gh_mirrors/ni/night点击查看免费下载相关推荐Stability AI生成模型深度实战指南从架构解析到专业部署Stability AI生成模型深度实战指南从架构解析到专业部署 在当今AI生成内容领域Stability AI的生成模型系列代表了最前沿的技术创新。这些模文档教程Go 夜读群讨论实录println 与 fmt.Print 输出乱序之谜及 Go 终端彩色输出实战Go 夜读群讨论实录println 与 fmt.Print 输出乱序之谜及 Go 终端彩色输出实战 本文是『Go 夜读』微信群 2019 03 07 技术讨论文档教程Go 夜读技术讨论基于 apns2 的高吞吐 iOS 批量推送架构实践Go 夜读技术讨论基于 apns2 的高吞吐 iOS 批量推送架构实践 本文源自 Go 夜读night reading go开源社区微信群 2018 08文档教程上一篇零基础预算管理BudgetZero 开源项目完整使用指南下一篇智能资源管家5分钟掌握浏览器媒体资源智能捕获与管理创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表