Go 博客
数据块
介绍
要通过网络传输数据结构或将其存储到文件中,必须对其进行编码,然后再进行解码。当然,有许多可用的编码:JSON、XML、Google 的 protocol buffers 等等。现在,Go 的 gob 包又提供了另一种。
为什么定义一种新的编码?这工作量很大,而且很冗余。为什么不直接使用现有的格式呢?嗯,首先,我们确实使用了!Go 有包支持所有刚才提到的编码(protocol buffer 包在一个单独的仓库中,但它是下载最频繁的包之一)。对于许多目的,包括与用其他语言编写的工具和系统通信,它们是正确的选择。
但对于 Go 特定的环境,例如用 Go 编写的两个服务器之间的通信,有机会构建一个更容易使用且可能更高效的东西。
Gobs 以一种外部定义、语言无关的编码无法做到的方式与语言协同工作。同时,也可以从现有系统中吸取经验。
目标
gob 包在设计时考虑了许多目标。
首先,也是最明显的,它必须非常易于使用。首先,因为 Go 具有反射机制,所以不需要单独的接口定义语言或“协议编译器”。数据结构本身就是包弄清楚如何编码和解码它所需要的全部信息。另一方面,这种方法意味着 gobs 与其他语言的协作效果不会那么好,但这没关系:gobs 毫不讳言地以 Go 为中心。
效率也很重要。以 XML 和 JSON 为代表的文本表示形式太慢,无法作为高效通信网络的中心。二进制编码是必需的。
Gob 流必须是自描述的。每个 gob 流从头开始读取时,都包含足够的信息,以便完全不了解其内容的代理可以解析整个流。此属性意味着您将始终能够解码存储在文件中的 gob 流,即使您早已忘记它代表什么数据。
从我们使用 Google protocol buffers 的经验中,还有一些东西需要学习。
Protocol buffer 的缺点
Protocol buffers 对 gobs 的设计产生了重大影响,但有三个特性是故意避免的。(撇开 protocol buffers 不是自描述的特性不谈:如果你不知道用于编码 protocol buffer 的数据定义,你可能无法解析它。)
首先,protocol buffers 只处理我们在 Go 中称为 struct 的数据类型。你不能在顶层编码整数或数组,只能编码一个包含字段的 struct。这似乎是一个毫无意义的限制,至少在 Go 中是这样。如果你只想发送一个整数数组,为什么必须先把它放到一个 struct 中呢?
其次,protocol buffer 定义可以指定每当编码或解码类型 T 的值时,字段 T.x
和 T.y
必须存在。尽管这些必需字段看起来是个好主意,但实现成本很高,因为编解码器在编码和解码时必须维护一个单独的数据结构,以便能够报告必需字段缺失的情况。它们也是一个维护问题。随着时间的推移,人们可能想要修改数据定义以删除必需字段,但这可能会导致现有数据的客户端崩溃。最好根本不要在编码中包含它们。(Protocol buffers 也有可选字段。但如果我们没有必需字段,那么所有字段都是可选的,就这样。关于可选字段,稍后还会详细介绍。)
第三个 protocol buffer 的缺点是默认值。如果 protocol buffer 省略了“默认”字段的值,那么解码后的结构会表现得好像该字段已设置为该值一样。当你有 getter 和 setter 方法来控制字段访问时,这个想法效果很好,但当容器只是一个普通的惯用 struct 时,就很难干净地处理了。必需字段也难以实现:默认值在哪里定义,它们有什么类型(文本是 UTF-8 吗?未解释的字节?浮点数有多少位?),尽管看起来很简单,但 protocol buffers 在设计和实现中存在一些复杂性。我们决定在 gobs 中省略它们,转而采用 Go 简单但有效的默认规则:除非你另行设置,否则它会具有该类型的“零值”——并且不需要传输。
因此,gobs 最终看起来像一种通用、简化的 protocol buffer。它们是如何工作的?
值
编码后的 gob 数据与 int8
和 uint16
等类型无关。相反,与 Go 中的常量有点类似,它的整数值是抽象的、无大小的数字,可以是带符号或无符号的。当你编码一个 int8
时,它的值会作为无大小、可变长度的整数传输。当你编码一个 int64
时,它的值也会作为无大小、可变长度的整数传输。(带符号和无符号是分开处理的,但同样的无大小特性也适用于无符号值。)如果两者都有值 7,那么在线上传输的比特位将是相同的。当接收方解码该值时,它会将其放入接收方的变量中,该变量可以是任意整数类型。因此,编码器可以发送一个来自 int8
的 7,但接收方可以将其存储在 int64
中。这很好:值是一个整数,只要它能容纳,一切都能正常工作。(如果不能容纳,就会出错。)这种与变量大小的解耦为编码带来了一些灵活性:随着软件的发展,我们可以扩展整数变量的类型,但仍然能够解码旧数据。
这种灵活性也适用于指针。传输前,所有指针都被展平。类型为 int8
、*int8
、**int8
、****int8
等的值都作为整数值传输,然后可以存储在任何大小的 int
中,或 *int
,或 ******int
等等。同样,这提供了灵活性。
灵活性还体现在,在解码 struct 时,只有编码器发送的字段才存储在目标中。给定值
type T struct{ X, Y, Z int } // Only exported fields are encoded and decoded.
var t = T{X: 7, Y: 0, Z: 8}
t 的编码只发送 7 和 8。因为它是零,所以 Y 的值甚至没有发送;无需发送零值。
接收方可以将该值解码到此结构中
type U struct{ X, Y *int8 } // Note: pointers to int8s
var u U
并获取一个只有 X 被设置(指向设置为 7 的 int8
变量地址)的 u 值;Z 字段被忽略——你会把它放在哪里?在解码 struct 时,字段按名称和兼容类型匹配,只有在两者中都存在的字段会受到影响。这种简单的方法巧妙地解决了“可选字段”问题:随着类型 T 通过添加字段而演进,旧的接收方仍然可以处理它们识别的那部分类型。因此,gobs 在没有任何额外机制或符号的情况下,提供了可选字段的重要结果——可扩展性。
从整数我们可以构建所有其他类型:字节、字符串、数组、切片、映射,甚至是浮点数。浮点值由其 IEEE 754 浮点位模式表示,存储为整数,只要您知道它们的类型(我们总是知道),这就能很好地工作。顺便说一下,该整数是以字节反转的顺序发送的,因为浮点数的常见值(例如小整数)在低位有很多零,我们可以避免传输这些零。
Go 使 gobs 成为可能的一个很好的特性是,它们允许您通过让您的类型实现 GobEncoder 和 GobDecoder 接口来定义自己的编码,其方式类似于 JSON 包的 Marshaler 和 Unmarshaler 以及 fmt 包的 Stringer 接口。此功能使得在传输数据时可以表示特殊特性、强制执行约束或隐藏秘密。详情请参见文档。
在线传输的类型
首次发送给定类型时,gob 包会在数据流中包含该类型的描述。实际上,发生的情况是使用编码器以标准 gob 编码格式编码一个内部 struct,该 struct 描述了类型并为其赋予一个唯一编号。(基本类型以及类型描述结构的布局由软件预定义以进行引导。)描述了类型之后,就可以通过其类型编号引用它。
因此,当我们发送第一个类型 T 时,gob 编码器会发送 T 的描述并为其标记一个类型编号,例如 127。然后,所有值(包括第一个值)都以该编号为前缀,因此 T 值的流看起来像
("define type id" 127, definition of type T)(127, T value)(127, T value), ...
这些类型编号使得描述递归类型并发送这些类型的值成为可能。因此,gobs 可以编码树等类型
type Node struct {
Value int
Left, Right *Node
}
(如何利用零值默认规则使其工作,即使 gobs 不表示指针,这是留给读者的一个练习。)
有了类型信息,除了引导类型集(这是一个明确的起点)之外,gob 流是完全自描述的。
编译一台机器
首次编码给定类型的值时,gob 包会构建一个专门用于该数据类型的小型解释器机器。它利用类型的反射来构建该机器,但机器一旦构建完成就不再依赖反射。该机器使用 unsafe 包和一些技巧以高速将数据转换为编码字节。它可以使用反射并避免 unsafe,但会显著变慢。(Go 对 protocol buffer 的支持也采用了类似的高速方法,其设计受到了 gobs 实现的影响。)相同类型的后续值使用已编译的机器,因此可以立即进行编码。
[更新:自 Go 1.4 起,gob 包不再使用 unsafe 包,性能略有下降。]
解码类似但更难。解码值时,gob 包持有一个字节切片,该切片表示要解码的给定编码器定义类型的值,以及一个用于解码的 Go 值。gob 包会为这对组合构建一台机器:线上传输的 gob 类型与提供的用于解码的 Go 类型交叉。然而,一旦解码机器构建完成,它又是一个无反射引擎,使用 unsafe 方法来获得最大速度。
使用
幕后有很多工作,但结果是一个高效、易于使用的数据传输编码系统。这里有一个完整的示例,展示了不同的编码和解码类型。请注意发送和接收值是多么容易;您只需要将值和变量提供给 gob 包,它就会完成所有工作。
package main
import (
"bytes"
"encoding/gob"
"fmt"
"log"
)
type P struct {
X, Y, Z int
Name string
}
type Q struct {
X, Y *int32
Name string
}
func main() {
// Initialize the encoder and decoder. Normally enc and dec would be
// bound to network connections and the encoder and decoder would
// run in different processes.
var network bytes.Buffer // Stand-in for a network connection
enc := gob.NewEncoder(&network) // Will write to network.
dec := gob.NewDecoder(&network) // Will read from network.
// Encode (send) the value.
err := enc.Encode(P{3, 4, 5, "Pythagoras"})
if err != nil {
log.Fatal("encode error:", err)
}
// Decode (receive) the value.
var q Q
err = dec.Decode(&q)
if err != nil {
log.Fatal("decode error:", err)
}
fmt.Printf("%q: {%d,%d}\n", q.Name, *q.X, *q.Y)
}
您可以在Go Playground 中编译并运行此示例代码。
rpc 包基于 gobs 构建,将这种编码/解码自动化转化为跨网络方法调用的传输方式。这是另一篇文章的主题。
详情
gob 包文档,尤其是文件 doc.go,扩展了此处描述的许多细节,并包含一个完整的示例,展示了编码如何表示数据。如果您对 gob 实现的内部机制感兴趣,那是一个很好的起点。
下一篇文章:Godoc:为 Go 代码编写文档
上一篇文章:C? Go? Cgo!
博客索引