Protobuf 与 JSON 对比
如果你已经从事过后端开发,甚至前端开发,那么你有 99.99% 的机会接触过 JSON
。JSON
无疑是目前最流行的数据模式,并且有很多原因解释了为什么它如此受欢迎。在本节中,我们将讨论 JSON
和 Protobuf
的优缺点,并解释它们在不同场景下的适用性。目标是客观地分析,因为作为工程师,我们需要为不同的任务选择合适的工具。
虽然我们可以写出几章内容来讨论每种技术的优缺点,但我们将把这些优势和劣势缩小到三个类别。这些类别是开发人员在开发应用程序时最关心的,具体如下:
-
序列化数据的大小:我们希望在通过网络发送数据时减少带宽的使用。
-
数据模式和序列化数据的可读性:我们希望能够拥有描述性的数据模式,以便新人或用户能够快速理解它,并且希望能够方便地查看序列化后的数据,以便进行调试或编辑。
-
模式的严格性:当 API 变得越来越庞大时,这通常会成为一个必要的要求,我们需要确保在不同应用程序之间发送和接收的数据类型是正确的。
序列化数据大小
在序列化中,很多用例中的 “圣杯” 就是减少数据的大小。这是因为大多数情况下,我们希望将数据通过网络发送到另一个应用程序,而数据越轻,应该越快到达对方。在这一领域,Protobuf
明显优于 JSON
。这是因为 JSON
序列化为文本,而 Protobuf
则序列化为二进制,因此它在压缩序列化数据的体积上有更多的优化空间。一个例子就是数字。如果你在 JSON
中将数字设置为 id
字段,你会得到如下所示的内容:
{ id: 123 }
首先,我们有一些样板内容(大括号),但最重要的是,我们有一个占用了三个字符或三个字节的数字。而在 Protobuf
中,如果我们将相同的值设置为相同的字段,我们将得到如下所示的十六进制表示:
在伴随的 GitHub 仓库的
例如:
|
现在,这可能看起来像是神秘的数字,但我们将在下一节中看到它是如何被编码成两个字节的。现在,两个字节而不是三个字节看起来可能微不足道,但想象一下在大规模使用时,这种差异会产生怎样的影响,可能会浪费掉数百万个字节。
可读性
关于数据模式序列化,另一个重要的方面是可读性。然而,可读性是一个比较宽泛的概念,特别是在 Protobuf
的上下文中。如我们所见,与 JSON
不同,Protobuf
将模式与序列化数据分开。我们在 .proto
文件中编写模式,然后序列化时会得到二进制数据。而在 JSON
中,模式就是实际的序列化数据。因此,为了更清晰和准确地表达可读性,我们将可读性分为两部分:模式的可读性和序列化数据的可读性。
关于模式的可读性,这更多是一个偏好的问题,但 Protobuf
在某些方面确实有其独特之处。首先,Protobuf
允许包含注释,这对于额外的文档说明要求非常有帮助。而 JSON
不允许在模式中添加注释,因此我们必须找到其他方式来提供文档,通常是通过 GitHub Wiki 或其他外部文档平台来实现。这会带来一个问题,因为这种文档很容易在项目和团队规模扩大时变得过时。只要稍有疏忽,文档可能就无法准确描述 API 的真实状态。使用 Protobuf
,虽然也有可能出现文档过时的情况,但由于文档更接近代码,这能提供更多的动机和意识去更新相关注释。
第二个使 Protobuf
更具可读性的特性是,它有明确的类型。虽然 JSON
也有类型,但它们是隐式的。你知道一个字段包含字符串是因为它的值被双引号包围,数字则是因为值只有数字,等等。而在 Protobuf
中,特别是对于数字,我们能够从类型中获得更多信息。如果我们有一个 int32
类型的字段,我们显然知道这是一个数字,并且知道它可以接受负数,而且还能了解该字段可以存储的数字范围。显式的类型不仅对安全性(稍后会讨论)非常重要,而且可以让开发者清楚地了解每个字段的详细信息,能够准确描述他们的模式,以满足业务需求。
对于模式的可读性,我想我们可以同意,Protobuf
在这方面是赢家,因为它可以作为自文档化的代码来编写,并且每个字段都能提供明确的类型。
而对于序列化数据的可读性,JSON
显然占据了优势。正如前面提到的,JSON
既是数据模式,又是序列化数据。你所看到的就是你所得到的。而 Protobuf
将数据序列化为二进制格式,即使你知道 Protobuf
如何进行序列化和反序列化,读取这些二进制数据也会变得非常困难。最终,这在可读性和序列化数据大小之间形成了权衡。Protobuf
在序列化数据上优于 JSON
,并且在数据模式的可读性上也更为明确。然而,如果你需要可以人工编辑的可读数据,Protobuf
就不是适合你的用例。
模式严格性
最后,第三个类别是模式的严格性。当你的团队和项目规模扩展时,这通常是一个非常有用的特性,因为它确保了模式的正确填充,并且对于某个目标语言,它缩短了开发者的反馈循环。
在 Protobuf
中,模式总是有效的,因为每个字段都有明确的类型,只能包含某些特定的值。我们不能将字符串传递给一个期待数字的字段,也不能将负数传递给一个只接受正数的字段。生成的代码会强制执行这一规则,对于动态语言来说是运行时检查,而对于静态类型语言来说,是编译时检查。在我们的案例中,由于 Go 是静态类型语言,我们会在编译时进行检查。
对于静态类型语言来说,模式还缩短了反馈循环,因为我们不需要依赖运行时检查,这些检查可能会触发也可能不会触发错误。相反,我们只会遇到编译时错误。这使得我们的软件更可靠,开发者可以确信,只要编译通过,数据集就已经有效。
在纯 JSON
中,我们无法在编译时确保模式的正确性。开发者通常会添加额外的配置,例如 JSON Schema,以在运行时进行这种保证。这会增加项目的复杂性,并要求每个开发者都要非常规范地处理,因为他们可能会在没有开发好模式的情况下就开始编写代码。而在 Protobuf
中,我们进行的是基于模式的开发。模式先行,之后我们的应用围绕着生成的类型展开。此外,我们在编译时就能确保设置的值是正确的,且不需要将这个设置复制到所有的微服务或子项目中。最终,我们将花费更少的时间在配置上,更多的时间用来思考我们的数据模式和数据编码。