Go 常见问题解答

关于在 Go 中实现协议缓冲区的一系列常见问题,以及每个问题的答案。

版本

github.com/golang/protobufgoogle.golang.org/protobuf 之间有什么区别?

github.com/golang/protobuf 模块是原始的 Go 协议缓冲区 API。

google.golang.org/protobuf 模块是此 API 的更新版本,专为简洁、易用和安全而设计。更新后的 API 的旗舰功能是对反射的支持以及用户界面 API 与底层实现的分离。

我们建议您在新代码中使用 google.golang.org/protobuf

github.com/golang/protobufv1.4.0 及更高版本封装了新的实现,并允许程序逐步采用新的 API。例如,在 github.com/golang/protobuf/ptypes 中定义的众所周知的类型只是较新模块中定义的类型的别名。因此,google.golang.org/protobuf/types/known/emptypbgithub.com/golang/protobuf/ptypes/empty 可以互换使用。

什么是 proto1proto2proto3

这些是协议缓冲区语言的修订版。它与 protobuf 的 Go 实现不同。

  • proto3 是当前版本的语言。这是最常用的语言版本。我们鼓励新代码使用 proto3。

  • proto2 是较旧版本的语言。尽管 proto3 已取代 proto2,但 proto2 仍然完全受支持。

  • proto1 是过时的语言版本。它从未作为开源发布。

有几种不同的 Message 类型。我应该使用哪一个?

常见问题

go install”:工作目录不是模块的一部分

在 Go 1.15 及更低版本上,您已设置环境变量 GO111MODULE=on,并且在模块目录外部运行 go install 命令。设置 GO111MODULE=auto,或取消设置环境变量。

在 Go 1.16 及更高版本上,可以通过指定显式版本在模块外部调用 go installgo install google.golang.org/protobuf/cmd/protoc-gen-go@latest

常量 -1 溢出 protoimpl.EnforceVersion

您正在使用生成的 .pb.go 文件,该文件需要较新版本的 "google.golang.org/protobuf" 模块。

使用以下命令更新到较新版本

go get -u google.golang.org/protobuf/proto

未定义:“github.com/golang/protobuf/proto”.ProtoPackageIsVersion4

您正在使用生成的 .pb.go 文件,该文件需要较新版本的 "github.com/golang/protobuf" 模块。

使用以下命令更新到较新版本

go get -u github.com/golang/protobuf/proto

什么是协议缓冲区命名空间冲突?

链接到 Go 二进制文件中的所有协议缓冲区声明都插入到全局注册表中。

每个 protobuf 声明(例如,枚举、枚举值或消息)都有一个绝对名称,它是 包名称.proto 源文件中声明的相对名称的串联(例如,my.proto.package.MyMessage.NestedMessage)。protobuf 语言假定所有声明都是普遍唯一的。

如果链接到 Go 二进制文件中的两个 protobuf 声明具有相同的名称,则会导致命名空间冲突,并且注册表无法按名称正确解析该声明。根据使用的 Go protobuf 版本,这将在初始化时引发 panic 或静默丢弃冲突,并在稍后的运行时导致潜在的错误。

我该如何修复协议缓冲区命名空间冲突?

修复命名空间冲突的最佳方法取决于发生冲突的原因。

命名空间冲突发生的常见方式

  • 供应商提供的 .proto 文件。 当单个 .proto 文件生成到两个或多个 Go 包中并链接到同一个 Go 二进制文件时,它会与生成的 Go 包中的每个 protobuf 声明发生冲突。当 .proto 文件是供应商提供的并且从中生成 Go 包,或者生成的 Go 包本身是供应商提供的时,通常会发生这种情况。用户应避免供应商提供,而应依赖于该 .proto 文件的集中式 Go 包。

    • 如果 .proto 文件由外部方拥有,并且缺少 go_package 选项,则您应与该 .proto 文件的所有者协调,以指定大多数用户都可以依赖的集中式 Go 包。
  • 缺少或通用的 proto 包名称。 如果 .proto 文件未指定包名称或使用过于通用的包名称(例如,“my_service”),则该文件中的声明很可能与宇宙中其他地方的其他声明冲突。我们建议每个 .proto 文件都有一个包名称,该名称经过刻意选择以使其具有普遍唯一性(例如,以公司名称为前缀)。

google.golang.org/protobuf 模块的 v1.26.0 开始,当 Go 程序启动时,如果其中链接了多个冲突的 protobuf 名称,则会报告硬错误。虽然最好修复冲突的来源,但可以通过以下两种方法之一立即解决此致命错误

  1. 在编译时。 可以使用链接器初始化的变量在编译时指定处理冲突的默认行为:go build -ldflags "-X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn"

  2. 在程序执行时。 可以在执行特定 Go 二进制文件时使用环境变量设置处理冲突的行为:GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn ./main

为什么 reflect.DeepEqual 在 protobuf 消息上的行为出乎意料?

生成的协议缓冲区消息类型包括内部状态,即使在等效消息之间也可能有所不同。

此外,reflect.DeepEqual 函数不了解协议缓冲区消息的语义,并且可能会报告不存在的差异。例如,包含 nil map 的 map 字段和包含零长度、非 nil map 的字段在语义上是等效的,但会被 reflect.DeepEqual 报告为不相等。

使用 proto.Equal 函数比较消息值。

在测试中,您还可以将 "github.com/google/go-cmp/cmp" 包与 protocmp.Transform() 选项一起使用。cmp 包可以比较任意数据结构,并且 cmp.Diff 生成人类可读的关于值之间差异的报告。

if diff := cmp.Diff(a, b, protocmp.Transform()); diff != "" {
  t.Errorf("unexpected difference:\n%v", diff)
}

海勒姆定律

什么是海勒姆定律,以及为什么它出现在此常见问题解答中?

海勒姆定律指出

当 API 的用户数量足够多时,您在合同中承诺什么并不重要:您的系统的所有可观察行为都将被某些人依赖。

最新版本的 Go 协议缓冲区 API 的设计目标是在可能的情况下避免提供我们无法保证在未来保持稳定的可观察行为。我们的理念是,在我们不作承诺的领域中,故意的不稳定性比给予稳定性的错觉更好,以免在项目可能长期依赖于该错误假设之后,未来发生变化。

为什么错误的文本不断变化?

依赖于错误的确切文本的测试是脆弱的,并且在文本更改时经常会中断。为了阻止在测试中不安全地使用错误文本,此模块生成的错误的文本被故意设计为不稳定的。

如果您需要确定错误是否由 protobuf 模块生成,我们保证所有错误都将根据 errors.Is 匹配 proto.Error

为什么 protojson 的输出不断变化?

我们不对 Go 实现的 协议缓冲区的 JSON 格式 的长期稳定性做出任何承诺。该规范仅指定什么是有效的 JSON,但未提供关于封送处理程序应如何精确地格式化给定消息的规范格式的规范。为了避免给人以输出是稳定的错觉,我们特意引入了细微的差异,以便字节到字节的比较很可能失败。

为了获得一定程度的输出稳定性,我们建议通过 JSON 格式化程序运行输出。

为什么 prototext 的输出不断变化?

我们不对 Go 实现的文本格式的长期稳定性做出任何承诺。protobuf 文本格式没有规范规范,我们希望保留未来改进 prototext 包输出的能力。由于我们不承诺包输出的稳定性,因此我们特意引入了不稳定性,以阻止用户依赖它。

为了获得一定程度的稳定性,我们建议通过 txtpbfmt 程序传递 prototext 的输出。可以使用 parser.Format 在 Go 中直接调用格式化程序。

杂项

如何使用协议缓冲区消息作为哈希键?

您需要规范序列化,其中协议缓冲区消息的封送输出保证在一段时间内保持稳定。不幸的是,目前尚不存在规范序列化的规范。您需要编写自己的规范,或者找到一种避免需要规范的方法。

我可以向 Go 协议缓冲区实现添加新功能吗?

也许可以。我们总是喜欢建议,但我们对添加新事物非常谨慎。

协议缓冲区的 Go 实现力求与其他语言实现保持一致。因此,我们倾向于避开仅针对 Go 过度专业化的功能。特定于 Go 的功能阻碍了协议缓冲区作为与语言无关的数据交换格式的目标。

除非您的想法特定于 Go 实现,否则您应该加入 protobuf 讨论组 并在那里提出建议。

如果您对 Go 实现有想法,请在我们的问题跟踪器上提交问题:https://github.com/golang/protobuf/issues

我可以向 MarshalUnmarshal 添加选项以自定义它吗?

仅当该选项在其他实现(例如,C++、Java)中存在时才行。协议缓冲区(二进制、JSON 和文本)的编码必须在各种实现中保持一致,以便以一种语言编写的程序能够读取由另一种语言编写的消息。

除非至少在另一种受支持的实现中存在等效选项,否则我们不会向 Go 实现添加任何会影响 Marshal 函数输出的数据或 Unmarshal 函数读取的数据的选项。

我可以自定义 protoc-gen-go 生成的代码吗?

一般来说,不行。协议缓冲区旨在成为一种与语言无关的数据交换格式,而特定于实现的自定义设置与该意图背道而驰。