Go 常见问题解答
版本
github.com/golang/protobuf
与 google.golang.org/protobuf
之间有什么区别?
github.com/golang/protobuf
模块是原始的 Go Protocol Buffers 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
中定义的知名类型(well-known types)只是在新模块中定义的类型的别名。因此,google.golang.org/protobuf/types/known/emptypb
和 github.com/golang/protobuf/ptypes/empty
可以互换使用。
什么是 proto1
、proto2
和 proto3
?
这些是 Protocol Buffers 语言 的修订版本。这与 Go 对 protobuf 的 实现 不同。
proto3
是当前的语言版本。这是最常用的语言版本。我们鼓励新代码使用 proto3。proto2
是一个较旧的语言版本。尽管已被 proto3 取代,proto2 仍然得到完全支持。proto1
是一个废弃的语言版本。它从未作为开源发布。
有几种不同的 Message
类型。我应该使用哪一种?
"google.golang.org/protobuf/proto".Message
是一个接口类型,由当前版本 Protocol Buffers 编译器生成的所有消息实现。操作任意消息的函数,例如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 Buffers API 定义的接口类型。所有生成的消息类型都实现了此接口,但该接口并未描述这些消息应有的行为。新代码应避免使用此类型。
常见问题
“go install
”:working directory is not part of a module
在 Go 1.15 及更低版本上,您设置了环境变量 GO111MODULE=on
并在模块目录之外运行 go install
命令。请将 GO111MODULE=auto
设置为 auto,或取消设置该环境变量。
在 Go 1.16 及更高版本上,可以在模块之外通过指定明确的版本来调用 go install
:go 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
什么是 Protocol Buffers 命名空间冲突?
链接到 Go 二进制文件中的所有 Protocol Buffers 声明都会被插入到全局注册表中。
每个 Protocol Buffers 声明(例如,枚举、枚举值或消息)都有一个绝对名称,它是将包名与声明在 .proto
源文件中的相对名称(例如,my.proto.package.MyMessage.NestedMessage
)连接而成。Protocol Buffers 语言假定所有声明都是全局唯一的。
如果链接到 Go 二进制文件中的两个 Protocol Buffers 声明具有相同的名称,则会导致命名空间冲突,并且注册表无法通过名称正确解析该声明。根据使用的 Go protobuf 版本,这可能会在初始化时引起 panic,或者默默地忽略冲突,并在运行时导致潜在的错误。
如何修复 Protocol Buffers 命名空间冲突?
修复命名空间冲突的最佳方法取决于冲突发生的原因。
命名空间冲突的常见原因
Vendored .proto 文件。当单个
.proto
文件被生成到两个或更多 Go 包并链接到同一个 Go 二进制文件时,它会在生成的 Go 包中的每个 Protocol Buffers 声明上发生冲突。这通常发生在.proto
文件被 vendor,并从其生成 Go 包,或者生成的 Go 包本身被 vendor 的情况下。用户应该避免 vendoring,而是依赖于该.proto
文件的集中式 Go 包。- 如果
.proto
文件由外部方拥有,并且缺少go_package
选项,那么您应该与该.proto
文件的所有者协调,指定一个可以由多数用户都依赖的集中式 Go 包。
- 如果
缺少或通用的 proto 包名。如果
.proto
文件未指定包名或使用了过于通用的包名(例如,“my_service”),则该文件中的声明很有可能与全局范围内的其他声明冲突。我们建议每个.proto
文件都有一个刻意选择的全局唯一包名(例如,以公司名称作为前缀)。
警告
回溯性地更改.proto
文件上的包名对于用作扩展字段的类型、存储在 google.protobuf.Any
中的类型或 gRPC Service 定义是不向后兼容的。从 google.golang.org/protobuf
模块的 v1.26.0 版本开始,当 Go 程序启动时链接了多个冲突的 Protocol Buffers 名称时,将报告硬错误。虽然最好修复冲突的来源,但致命错误可以通过以下两种方式之一立即解决:
编译时。处理冲突的默认行为可以在编译时通过链接器初始化的变量来指定:
go build -ldflags "-X google.golang.org/protobuf/reflect/protoregistry.conflictPolicy=warn"
程序执行时。执行特定 Go 二进制文件时处理冲突的行为可以通过环境变量设置:
GOLANG_PROTOBUF_REGISTRATION_CONFLICT=warn ./main
为什么 reflect.DeepEqual
在处理 Protocol Buffers 消息时表现异常?
生成的 Protocol Buffers 消息类型包含内部状态,即使是等效的消息,此状态也可能不同。
此外,reflect.DeepEqual
函数不知道 Protocol Buffers 消息的语义,并且可能会报告实际上不存在的差异。例如,包含 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 Protocol Buffers API 的一个设计目标是尽可能避免提供我们无法保证未来保持稳定的可观察行为。我们的理念是,在我们不作任何承诺的领域故意引入不稳定性,好过制造稳定性的假象,而这种假象在项目可能长期依赖于这个错误假设后又在将来发生变化。
为什么错误文本会不断变化?
依赖于精确错误文本的测试很脆弱,并且在文本更改时经常失效。为了阻止在测试中不安全地使用错误文本,此模块生成的错误文本被故意设计为不稳定。
如果您需要判断错误是否由 protobuf
模块产生,我们保证所有错误都将根据 errors.Is
匹配 proto.Error
。
为什么 protojson
的输出会不断变化?
我们不承诺 Go 实现 Protocol Buffers 的 JSON 格式 的长期稳定性。规范只规定了什么是有效的 JSON,但没有规定编组器应该如何 *精确地* 格式化给定消息的 *规范* 格式。为了避免制造输出稳定的假象,我们故意引入了微小的差异,以便逐字节比较很可能会失败。
为了获得一定程度的输出稳定性,我们建议通过 JSON 格式化工具处理输出。
为什么 prototext
的输出会不断变化?
我们不承诺 Go 实现文本格式的长期稳定性。Protocol Buffers 文本格式没有规范的规范,我们希望保留未来改进 prototext
包输出的能力。由于我们不承诺包输出的稳定性,因此我们故意引入了不稳定性,以阻止用户依赖它。
为了获得一定程度的稳定性,我们建议将 prototext
的输出通过 txtpbfmt
程序处理。可以在 Go 中使用 parser.Format
直接调用格式化器。
其他
如何将 Protocol Buffers 消息用作哈希键?
您需要规范序列化,即 Protocol Buffers 消息的编组输出保证随时间推移保持稳定。不幸的是,目前尚无规范序列化的规范。您需要自己编写或寻找避免使用它的方法。
我可以在 Go Protocol Buffers 实现中添加新特性吗?
也许吧。我们总是乐于接受建议,但对于添加新功能我们非常谨慎。
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 旨在成为一种与语言无关的数据交换格式,而实现特定的自定义与此意图相悖。