Go 常见问题解答
版本
github.com/golang/protobuf
和 google.golang.org/protobuf
有什么区别?
github.com/golang/protobuf
模块是最初的 Go protocol buffer API。
google.golang.org/protobuf
模块是此 API 的更新版本,旨在实现简单性、易用性和安全性。更新后 API 的旗舰功能是支持反射以及将面向用户的 API 与底层实现分离。
我们建议您在新代码中使用 google.golang.org/protobuf
。
github.com/golang/protobuf
的 v1.4.0
及更高版本包装了新的实现,并允许程序逐步采用新的 API。例如,github.com/golang/protobuf/ptypes
中定义的知名类型只是新模块中定义的类型的别名。因此,google.golang.org/protobuf/types/known/emptypb
和 github.com/golang/protobuf/ptypes/empty
可以互换使用。
proto1
、proto2
、proto3
和 editions 是什么?
这些是 protocol buffer *语言* 的修订版。它与 protobuf 的 Go *实现* 不同。
Editions 是编写 Protocol Buffers 的最新且推荐的方式。新功能将作为新版本的一部分发布。有关更多信息,请参阅 Protocol Buffer Editions。
proto3
是该语言的旧版本。我们鼓励新代码使用 editions。proto2
是该语言的旧版本。尽管已被 proto3 和 editions 取代,但 proto2 仍然得到完全支持。proto1
是该语言的过时版本。它从未作为开源发布。
有好几种不同的 Message
类型。我应该使用哪一种?
"google.golang.org/protobuf/proto".Message
是一个接口类型,由当前版本的 protocol buffer 编译器生成的所有消息实现。对任意消息进行操作的函数,例如proto.Marshal
或proto.Clone
,接受或返回此类型。"google.golang.org/protobuf/reflect/protoreflect".Message
是一个描述消息反射视图的接口类型。在
proto.Message
上调用ProtoReflect
方法以获取protoreflect.Message
。"google.golang.org/protobuf/reflect/protoreflect".ProtoMessage
是"google.golang.org/protobuf/proto".Message
的别名。这两种类型可以互换。"github.com/golang/protobuf/proto".Message
是由旧版 Go protocol buffer API 定义的接口类型。所有生成的消息类型都实现此接口,但该接口不描述这些消息的预期行为。新代码应避免使用此类型。
常见问题
“go install
”: working directory is not part of a module
(工作目录不属于模块)
在 Go 1.15 及更低版本中,您设置了环境变量 GO111MODULE=on
并在模块目录之外运行 go install
命令。请设置 GO111MODULE=auto
,或取消设置该环境变量。
在 Go 1.16 及更高版本中,可以通过指定显式版本在模块外部调用 go install
:go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
constant -1 overflows protoimpl.EnforceVersion (常量 -1 溢出 protoimpl.EnforceVersion)
您正在使用一个生成的 .pb.go
文件,该文件需要更新版本的 "google.golang.org/protobuf"
模块。
使用以下命令更新到较新版本
go get -u google.golang.org/protobuf/proto
undefined: "github.com/golang/protobuf/proto".ProtoPackageIsVersion4 (未定义)
您正在使用一个生成的 .pb.go
文件,该文件需要更新版本的 "github.com/golang/protobuf"
模块。
使用以下命令更新到较新版本
go get -u github.com/golang/protobuf/proto
什么是 protocol buffer 命名空间冲突?
链接到 Go 二进制文件中的所有 protocol buffers 声明都会被插入到一个全局注册表中。
每个 protobuf 声明(例如,枚举、枚举值或消息)都有一个绝对名称,它是 包名称 与 .proto
源文件中声明的相对名称的串联(例如,my.proto.package.MyMessage.NestedMessage
)。protobuf 语言假定所有声明都是普遍唯一的。
如果链接到 Go 二进制文件中的两个 protobuf 声明具有相同的名称,则会导致命名空间冲突,注册表将无法按名称正确解析该声明。根据所使用的 Go protobufs 版本,这将在初始化时引发 panic 或静默地丢弃冲突,从而在运行时后期导致潜在的错误。
如何修复 protocol buffer 命名空间冲突?
修复命名空间冲突的最佳方法取决于冲突发生的原因。
命名空间冲突的常见发生方式
Vendored 的 .proto 文件。当单个
.proto
文件生成到两个或多个 Go 包中并链接到同一个 Go 二进制文件中时,它会在生成的 Go 包中的每个 protobuf 声明上发生冲突。这通常发生在.proto
文件被 vendored 并且从中生成了一个 Go 包,或者生成的 Go 包本身被 vendored 时。用户应避免 vendoring,而应依赖该.proto
文件的集中式 Go 包。- 如果
.proto
文件由外部方拥有并且缺少go_package
选项,则您应与该.proto
文件的所有者协调,以指定一个多数用户都可以依赖的集中式 Go 包。
- 如果
缺少或通用的 proto 包名称。如果
.proto
文件未指定包名称或使用了过于通用的包名称(例如,“my_service”),那么该文件中的声明很有可能与宇宙中其他地方的其他声明发生冲突。我们建议每个.proto
文件都有一个特意选择的普遍唯一的包名称(例如,以公司名称为前缀)。
警告
对于用作扩展字段、存储在google.protobuf.Any
中或用于 gRPC 服务定义的类型,追溯性地更改 .proto
文件上的包名称是不向后兼容的。从 google.golang.org/protobuf
模块的 v1.26.0 开始,当一个 Go 程序启动时,如果链接了多个冲突的 protobuf 名称,将会报告一个硬错误。虽然最好修复冲突的源头,但可以通过以下两种方式之一立即解决此致命错误
在编译时。处理冲突的默认行为可以在编译时通过链接器初始化的变量指定:
go build -ldflags "-X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn"
在程序执行时。执行特定 Go 二进制文件时处理冲突的行为可以通过环境变量设置:
GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn ./main
如何使用 protocol buffer editions?
要使用 protobuf edition,您必须在 .proto
文件中指定 edition。例如,要使用 2023 edition,请将以下内容添加到您的 .proto
文件顶部
edition = "2023";
然后,protocol buffer 编译器将生成与指定 edition 兼容的 Go 代码。使用 editions,您还可以为您的 .proto
文件启用或禁用特定功能。有关更多信息,请参阅 Protocol Buffer Editions。
如何控制生成的 Go 代码的行为?
使用 editions,您可以通过在 .proto
文件中启用或禁用特定功能来控制生成的 Go 代码的行为。例如,要为您的实现设置 API 行为,您可以将以下内容添加到您的 .proto
文件中
edition = "2023";
option features.(pb.go).api_level = API_OPAQUE;
当 api_level
设置为 API_OPAQUE
时,protocol buffer 编译器生成的 Go 代码会隐藏结构体字段,使其无法再被直接访问。取而代之的是,会创建新的访问器方法来获取、设置或清除字段。
有关可用功能及其描述的完整列表,请参阅 Editions 的功能。
为什么 reflect.DeepEqual
对 protobuf 消息的行为出乎意料?
生成的 protocol buffer 消息类型包含内部状态,即使在等效消息之间也可能不同。
此外,reflect.DeepEqual
函数不知道 protocol buffer 消息的语义,并且可能在不存在差异的地方报告差异。例如,包含 nil
映射的映射字段和包含零长度、非 nil
映射的映射字段在语义上是等效的,但 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)
}
海勒姆定律 (Hyrum's Law)
什么是海勒姆定律,为什么它会出现在这个常见问题解答中?
海勒姆定律 指出
当一个 API 有足够多的用户时,你在合同中承诺什么并不重要:你系统的所有可观察行为都会被某些人所依赖。
最新版本的 Go protocol buffer API 的一个设计目标是,在可能的情况下,避免提供我们无法承诺在未来保持稳定的可观察行为。我们的理念是,在我们不作任何承诺的领域故意制造不稳定性,比给人一种稳定性的假象,然后这种假象在未来某个项目可能已经长期依赖这种错误假设之后发生改变要好。
为什么错误的文本内容一直在变?
依赖于错误确切文本的测试是脆弱的,并且当文本改变时经常会中断。为了阻止在测试中不安全地使用错误文本,此模块产生的错误文本是故意不稳定的。
如果您需要确定某个错误是否由 protobuf
模块产生,我们保证所有错误都将根据 errors.Is
匹配 proto.Error
。
为什么 protojson
的输出一直在变?
我们对 Go 实现的 protocol buffers 的 JSON 格式 的长期稳定性不作任何承诺。该规范仅规定了什么是有效的 JSON,但没有为编组器应如何*确切地*格式化给定消息提供*规范*格式的规范。为了避免给人一种输出是稳定的假象,我们故意引入微小的差异,以便逐字节比较很可能会失败。
为了获得一定程度的输出稳定性,我们建议通过 JSON 格式化程序来运行输出。
为什么 prototext
的输出一直在变?
我们对 Go 实现的文本格式的长期稳定性不作任何承诺。没有 protobuf 文本格式的规范性规范,我们希望保留在未来改进 prototext
包输出的能力。由于我们不承诺包输出的稳定性,我们故意引入了不稳定性,以阻止用户依赖它。
为了获得一定程度的稳定性,我们建议将 prototext
的输出传递给 txtpbfmt
程序。该格式化程序可以直接在 Go 中使用 parser.Format
调用。
其他
如何将 protocol buffer 消息用作哈希键?
您需要规范序列化,即 protocol buffer 消息的编组输出保证随时间推移是稳定的。不幸的是,目前不存在规范序列化的规范。您需要自己编写或找到避免需要它的方法。
我可以为 Go protocol buffer 实现添加新功能吗?
也许吧。我们总是喜欢建议,但我们对添加新事物非常谨慎。
Go 实现的 protocol buffers 努力与其他语言的实现保持一致。因此,我们倾向于避开过于专门针对 Go 的功能。特定于 Go 的功能阻碍了 protocol buffers 成为一种与语言无关的数据交换格式的目标。
除非您的想法特定于 Go 实现,否则您应该加入 protobuf 讨论组并在那里提出建议。
如果您对 Go 实现有想法,请在我们的问题跟踪器上提交一个问题:https://github.com/golang/protobuf/issues
我可以为 Marshal
或 Unmarshal
添加一个选项来自定义它吗?
仅当该选项在其他实现(例如 C++、Java)中存在时才可以。protocol buffers 的编码(二进制、JSON 和文本)必须在各个实现中保持一致,因此用一种语言编写的程序能够读取由另一种语言编写的消息。
除非在至少一个其他支持的实现中存在等效选项,否则我们不会向 Go 实现中添加任何影响 Marshal
函数输出的数据或 Unmarshal
函数读取的数据的选项。
我可以自定义 protoc-gen-go
生成的代码吗?
总的来说,不可以。Protocol buffers 旨在成为一种与语言无关的数据交换格式,而特定于实现的自定义与这一意图背道而驰。