请点击上方蓝字TonyBai订阅公众号!
Go标准库中的encoding/json包,作为Go社区广泛使用的JSON处理工具,至今已走过十余年[1]。凭借其将JSON数据与原生Go类型相互转换的能力、通过struct tag自定义字段表示的灵活性,以及Go类型自定义JSON格式的特性,赢得了Go开发者的青睐。
然而,随着时间的推移,encoding/json的局限性也逐渐显现。孤立地解决这些问题可能会导致非正交特性之间产生意外的交互。因此,Go团队在2023年下旬发起了关于encoding/json/v2的讨论[2],旨在全面审视现有encoding/json包,并提出一个面向未来十年的Go JSON处理方案,打造一个 JSON处理新引擎。
经过近一年多的讨论、设计调整以及参考实现的优化,Go团队于近期正式提出了关于encoding/json/v2的提案issue[3]。在该issue中,Go团队梳理并总结了讨论结果以及初始设计与后续调整之间的差异,以供Go社区进一步审阅与反馈。
为了让大家更好地了解该提案issue的核心内容,本文将对提案的背景、主要内容、相对于v1版本的主要改进,以及与v1版本的联系等进行全面介绍,希望通过这篇文章,大家能够及时了解到Go标准库json包的演进与变化。
1. 提案背景:现有encoding/json包的局限与改进的需求
在过去十年中,开发者在使用encoding/json的过程中,逐渐意识到了它在功能、API设计、性能和行为上存在的不足。这些问题可以归纳为以下几个方面,也正是encoding/json/v2提案希望解决的核心痛点:
1.1 功能缺失 (Missing functionality)
尽管encoding/json功能完善,但仍存在一些重要的功能缺失,社区也为此提出了诸多Feature Request,其中最突出的包括:
time.Time的自定义格式化 (#21990): 缺乏灵活的方式来指定time.Time类型在JSON中的格式,例如自定义日期时间字符串格式。
Marshal时忽略特定Go值 (#11939, #22480, #50480, #29310, #52803, #45669): 现有的omitempty标签在某些场景下无法满足需求,开发者希望更精细地控制哪些Go值在Marshal时被忽略,例如忽略零值、空值或特定条件下的值。Go 1.24版本[4]增加了omitzero tag[5],将在一定层度缓解这个问题。
将nil切片和mapMarshal为空JSON数组和对象 (#37711, #27589): encoding/json默认将nil切片和mapMarshal为JSONnull,但在某些场景下,开发者更期望将其Marshal为空的JSON数组[]和对象{}。
无Go嵌入的Inline类型 (#6213): 希望能够更灵活地将Go类型内联到JSON对象中,而无需依赖Go的struct嵌入机制。
虽然这些功能缺失大部分可以通过向现有encoding/json包添加新功能来解决,但可能会导致API变得臃肿和复杂。
1.2 API 设计缺陷 (API deficiencies)
encoding/json的API设计存在一些尖锐或限制性的问题,影响了开发者的使用体验:
难以正确地从io.Reader进行Unmarshal: 常用的json.NewDecoder(r).Decode(v)方法并不能正确处理JSON payload末尾的垃圾数据 (#36225),容易导致数据解析错误。
Marshal和Unmarshal函数无法使用Options: 虽然Encoder和Decoder类型支持Options配置,但Marshal和Unmarshal函数却无法使用,同样,实现Marshaler和Unmarshaler接口的类型也无法利用Options,缺乏选项配置的传递机制(#41144)。
Compact, Indent, HTMLEscape函数输出目标受限: 这些格式化函数只能将结果写入bytes.Buffer,而不是更灵活的[]byte或io.Writer,限制了函数的使用场景。
这些API缺陷可以通过向现有encoding/json包引入新的API来修复,但这可能会导致同一个任务在同一个包中存在多种不同的实现方式,增加学习成本和使用困惑。
1.3 性能限制 (Performance limitations)
encoding/json的性能表现一直备受关注,存在诸多限制性能提升的因素:
MarshalJSON接口: 强制实现者分配[]byte返回值,且encoding/json需要再次解析返回值以验证JSON的有效性并重新格式化,造成不必要的性能开销。
UnmarshalJSON接口: 要求提供完整的JSON value,导致encoding/json需要预先完整解析JSON值以确定边界,之后UnmarshalJSON方法本身还需要再次解析,如果UnmarshalJSON递归调用Unmarshal,则会导致O(N²)的性能退化,例如Kubernetes kube-openapi项目在Unmarshalspec.Swagger时遇到的性能瓶颈 (kubernetes/kube-openapi#315)。
Encoder.WriteToken: 缺乏流式Encoder API,虽然提案已被接受(#40127),但尚未实现,且可能同样存在性能问题。
Decoder.Token: Token类型是一个接口,可以容纳多种类型 (Delim, bool, float64, Number, string, nil),当boxing数字或字符串到Token接口类型时,会频繁发生内存分配 (#40128)。
缺乏真正的流式处理: 即使Encoder.Encode和Decoder.Decode方法操作io.Writer和io.Reader,它们仍然会将整个JSON value缓冲到内存中,需要二次扫描JSON,与流式处理的初衷背道而驰 (#33714, #7872, #11046)。
encoding/json应该默认以真正的流式方式操作io.Writer和io.Reader。缓冲整个JSON value违背了使用io.Reader和i