Go 生成的代码指南
突出显示了 proto2 和 proto3 生成的代码之间的任何差异 - 请注意,这些差异存在于本文档中所述的生成的代码中,而不是基本 API 中,这两个版本中的基本 API 是相同的。在阅读本文档之前,您应该阅读proto2 语言指南和/或proto3 语言指南。
编译器调用
协议缓冲区编译器需要一个插件来生成 Go 代码。使用 Go 1.16 或更高版本运行以下命令来安装它:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
这将在 $GOBIN
中安装一个 protoc-gen-go
二进制文件。设置 $GOBIN
环境变量以更改安装位置。它必须在您的 $PATH
中,以便协议缓冲区编译器能够找到它。
协议缓冲区编译器在使用 go_out
标志调用时生成 Go 输出。go_out
标志的参数是您希望编译器将 Go 输出写入的目录。编译器为每个 .proto
文件输入创建一个源文件。输出文件的名称是通过将 .proto
扩展名替换为 .pb.go
创建的。
生成的 .pb.go
文件在输出目录中的放置位置取决于编译器标志。有几种输出模式
- 如果指定了
paths=import
标志,则输出文件将放置在以 Go 包的导入路径命名的目录中(例如.proto
文件中的go_package
选项提供的目录)。例如,一个输入文件protos/buzz.proto
,其 Go 导入路径为example.com/project/protos/fizz
,会导致输出文件位于example.com/project/protos/fizz/buzz.pb.go
。如果未指定paths
标志,这是默认的输出模式。 - 如果指定了
module=$PREFIX
标志,则输出文件将放置在以 Go 包的导入路径命名的目录中(例如.proto
文件中的go_package
选项提供的目录),但输出文件名中将删除指定的目录前缀。例如,一个输入文件protos/buzz.proto
,其 Go 导入路径为example.com/project/protos/fizz
,并且example.com/project
指定为module
前缀,会导致输出文件位于protos/fizz/buzz.pb.go
。生成模块路径之外的任何 Go 包都会导致错误。此模式对于将生成的文件直接输出到 Go 模块中很有用。 - 如果指定了
paths=source_relative
标志,则输出文件将放置在与输入文件相同的相对目录中。例如,一个输入文件protos/buzz.proto
会导致输出文件位于protos/buzz.pb.go
。
通过在调用 protoc
时传递 go_opt
标志,可以提供特定于 protoc-gen-go
的标志。可以传递多个 go_opt
标志。例如,运行以下命令时
protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto bar/baz.proto
编译器将从 src
目录中的 foo.proto
和 bar/baz.proto
读取输入文件,并将输出文件 foo.pb.go
和 bar/baz.pb.go
写入 out
目录。编译器会在必要时自动创建嵌套的输出子目录,但不会创建输出目录本身。
包
为了生成 Go 代码,必须为每个 .proto
文件(包括被正在生成的 .proto
文件传递依赖的文件)提供 Go 包的导入路径。有两种方法可以指定 Go 导入路径
- 在
.proto
文件中声明它,或者 - 在调用
protoc
时在命令行上声明它。
我们建议在 .proto
文件中声明它,以便 .proto
文件的 Go 包可以与 .proto
文件本身集中标识,并简化调用 protoc
时传递的标志集。如果给定 .proto
文件的 Go 导入路径同时由 .proto
文件本身和命令行提供,则后者优先于前者。
通过使用 go_package
选项声明 Go 包的完整导入路径,可以在 .proto
文件中本地指定 Go 导入路径。示例用法
option go_package = "example.com/project/protos/fizz";
在调用编译器时,可以通过传递一个或多个 M${PROTO_FILE}=${GO_IMPORT_PATH}
标志在命令行上指定 Go 导入路径。示例用法
protoc --proto_path=src \
--go_opt=Mprotos/buzz.proto=example.com/project/protos/fizz \
--go_opt=Mprotos/bar.proto=example.com/project/protos/foo \
protos/buzz.proto protos/bar.proto
由于所有 .proto
文件与其 Go 导入路径的映射可能非常大,因此这种指定 Go 导入路径的方式通常由某些构建工具(例如 Bazel)执行,这些工具可以控制整个依赖项树。如果给定 .proto
文件存在重复条目,则指定最后一个条目优先。
对于 go_package
选项和 M
标志,该值可以包含一个显式包名称,该名称用分号与导入路径分隔。例如:"example.com/protos/foo;package_name"
。不建议使用此用法,因为默认情况下将以合理的方式从导入路径派生包名称。
导入路径用于确定当一个 .proto
文件导入另一个 .proto
文件时必须生成哪些导入语句。例如,如果 a.proto
导入 b.proto
,则生成的 a.pb.go
文件需要导入包含生成的 b.pb.go
文件的 Go 包(除非两个文件在同一个包中)。导入路径还用于构造输出文件名。有关详细信息,请参阅上面的“编译器调用”部分。
Go 导入路径与 .proto
文件中的package
说明符之间没有关联。后者仅与 protobuf 命名空间相关,而前者仅与 Go 命名空间相关。此外,Go 导入路径与 .proto
导入路径之间没有关联。
消息
给定一个简单的消息声明
message Artist {}
协议缓冲区编译器会生成一个名为 Artist
的结构体。*Artist
实现 proto.Message
接口。
proto
包提供对消息进行操作的函数,包括转换为和从二进制格式转换。
proto.Message
接口定义了一个 ProtoReflect
方法。此方法返回一个 protoreflect.Message
,该方法提供了消息的基于反射的视图。
optimize_for
选项不影响 Go 代码生成器的输出。
嵌套类型
可以在另一个消息内部声明消息。例如
message Artist {
message Name {
}
}
在这种情况下,编译器会生成两个结构体:Artist
和 Artist_Name
。
字段
协议缓冲区编译器为在消息中定义的每个字段生成一个结构体字段。此字段的确切性质取决于其类型以及它是单数、重复、映射还是 oneof 字段。
请注意,生成的 Go 字段名称始终使用驼峰式命名法,即使 .proto
文件中的字段名称使用带下划线的小写字母(它应该这样)。大小写转换的工作原理如下
- 第一个字母大写以供导出。如果第一个字符是下划线,则将其删除并在前面加上大写 X。
- 如果内部下划线后跟一个小写字母,则删除下划线,并将后续字母大写。
因此,proto 字段 birth_year
在 Go 中变为 BirthYear
,_birth_year_2
变为 XBirthYear_2
。
单数标量字段 (proto2)
对于以下任何字段定义
optional int32 birth_year = 1;
required int32 birth_year = 1;
编译器会生成一个具有名为 BirthYear
的 *int32
字段的结构体,以及一个 GetBirthYear()
访问器方法,该方法返回 Artist
中的 int32
值,或者如果字段未设置则返回默认值。如果未显式设置默认值,则使用该类型的零值(数字为 0
,字符串为空字符串)。
对于其他标量字段类型(包括bool
、bytes
和string
),*int32
将根据标量值类型表替换为相应的Go类型。
单数标量字段 (proto3)
对于以下字段定义
int32 birth_year = 1;
optional int32 first_active_year = 2;
编译器将生成一个包含名为BirthYear
的int32
字段的结构体,以及一个访问器方法GetBirthYear()
,该方法返回birth_year
中的int32
值,如果字段未设置,则返回该类型的零值(数字为0
,字符串为空字符串)。
对于其他标量字段类型(包括bool
、bytes
和string
),int32
将根据标量值类型表替换为相应的Go类型。proto中未设置的值将表示为该类型的零值(数字为0
,字符串为空字符串)。
单数消息字段
给定消息类型
message Band {}
对于包含Band
字段的消息
// proto2
message Concert {
optional Band headliner = 1;
// The generated code is the same result if required instead of optional.
}
// proto3
message Concert {
Band headliner = 1;
}
编译器将生成一个Go结构体
type Concert struct {
Headliner *Band
}
消息字段可以设置为nil
,这意味着该字段未设置,有效地清除了该字段。这与将值设置为消息结构体的“空”实例并不等效。
编译器还会生成一个func (m *Concert) GetHeadliner() *Band
辅助函数。如果m
为nil或headliner
未设置,则此函数返回一个nil
*Band
。这使得可以在不进行中间nil
检查的情况下链接获取调用。
var m *Concert // defaults to nil
log.Infof("GetFoundingYear() = %d (no panic!)", m.GetHeadliner().GetFoundingYear())
重复字段
每个重复字段在Go结构体中生成一个T
字段的切片,其中T
是字段的元素类型。对于包含重复字段的消息
message Concert {
// Best practice: use pluralized names for repeated fields:
// /programming-guides/style#repeated-fields
repeated Band support_acts = 1;
}
编译器将生成Go结构体
type Concert struct {
SupportActs []*Band
}
同样,对于字段定义repeated bytes band_promo_images = 1;
,编译器将生成一个包含名为BandPromoImage
的[][]byte
字段的Go结构体。对于重复的枚举(如repeated MusicGenre genres = 2;
),编译器将生成一个包含名为Genre
的[]MusicGenre
字段的结构体。
以下示例显示了如何设置字段
concert := &Concert{
SupportActs: []*Band{
{}, // First element.
{}, // Second element.
},
}
要访问该字段,您可以执行以下操作
support := concert.GetSupportActs() // support type is []*Band.
b1 := support[0] // b1 type is *Band, the first element in support_acts.
映射字段
每个映射字段在结构体中生成一个类型为map[TKey]TValue
的字段,其中TKey
是字段的键类型,TValue
是字段的值类型。对于包含映射字段的消息
message MerchItem {}
message MerchBooth {
// items maps from merchandise item name ("Signed T-Shirt") to
// a MerchItem message with more details about the item.
map<string, MerchItem> items = 1;
}
编译器将生成Go结构体
type MerchBooth struct {
Items map[string]*MerchItem
}
Oneof 字段
对于oneof字段,protobuf编译器会生成一个具有接口类型isMessageName_MyField
的单个字段。它还会为oneof中的每个单一字段生成一个结构体。这些都实现了此isMessageName_MyField
接口。
对于包含oneof字段的消息
package account;
message Profile {
oneof avatar {
string image_url = 1;
bytes image_data = 2;
}
}
编译器将生成以下结构体
type Profile struct {
// Types that are valid to be assigned to Avatar:
// *Profile_ImageUrl
// *Profile_ImageData
Avatar isProfile_Avatar `protobuf_oneof:"avatar"`
}
type Profile_ImageUrl struct {
ImageUrl string
}
type Profile_ImageData struct {
ImageData []byte
}
*Profile_ImageUrl
和*Profile_ImageData
都通过提供一个空的isProfile_Avatar()
方法来实现isProfile_Avatar
。
以下示例显示了如何设置字段
p1 := &account.Profile{
Avatar: &account.Profile_ImageUrl{ImageUrl: "http://example.com/image.png"},
}
// imageData is []byte
imageData := getImageData()
p2 := &account.Profile{
Avatar: &account.Profile_ImageData{ImageData: imageData},
}
要访问该字段,您可以对值使用类型切换来处理不同的消息类型。
switch x := m.Avatar.(type) {
case *account.Profile_ImageUrl:
// Load profile image based on URL
// using x.ImageUrl
case *account.Profile_ImageData:
// Load profile image based on bytes
// using x.ImageData
case nil:
// The field is not set.
default:
return fmt.Errorf("Profile.Avatar has unexpected type %T", x)
}
编译器还会生成获取方法func (m *Profile) GetImageUrl() string
和func (m *Profile) GetImageData() []byte
。每个获取函数都返回该字段的值,如果未设置,则返回零值。
枚举
给定一个类似以下的枚举
message Venue {
enum Kind {
KIND_UNSPECIFIED = 0;
KIND_CONCERT_HALL = 1;
KIND_STADIUM = 2;
KIND_BAR = 3;
KIND_OPEN_AIR_FESTIVAL = 4;
}
Kind kind = 1;
// ...
}
协议缓冲区编译器将生成一个类型和一系列具有该类型的常量
type Venue_Kind int32
const (
Venue_KIND_UNSPECIFIED Venue_Kind = 0
Venue_KIND_CONCERT_HALL Venue_Kind = 1
Venue_KIND_STADIUM Venue_Kind = 2
Venue_KIND_BAR Venue_Kind = 3
Venue_KIND_OPEN_AIR_FESTIVAL Venue_Kind = 4
)
对于消息中的枚举(如上面的枚举),类型名称以消息名称开头
type Venue_Kind int32
对于包级枚举
enum Genre {
GENRE_UNSPECIFIED = 0;
GENRE_ROCK = 1;
GENRE_INDIE = 2;
GENRE_DRUM_AND_BASS = 3;
// ...
}
Go类型名称不会从proto枚举名称修改
type Genre int32
此类型具有一个String()
方法,该方法返回给定值的名称。
Enum()
方法使用给定值初始化新分配的内存,并返回相应的指针
func (Genre) Enum() *Genre
协议缓冲区编译器为枚举中的每个值生成一个常量。对于消息中的枚举,常量以包含消息的名称开头
const (
Venue_KIND_UNSPECIFIED Venue_Kind = 0
Venue_KIND_CONCERT_HALL Venue_Kind = 1
Venue_KIND_STADIUM Venue_Kind = 2
Venue_KIND_BAR Venue_Kind = 3
Venue_KIND_OPEN_AIR_FESTIVAL Venue_Kind = 4
)
对于包级枚举,常量以枚举名称开头
const (
Genre_GENRE_UNSPECIFIED Genre = 0
Genre_GENRE_ROCK Genre = 1
Genre_GENRE_INDIE Genre = 2
Genre_GENRE_DRUM_AND_BASS Genre = 3
)
protobuf编译器还会生成从整数值到字符串名称的映射,以及从名称到值的映射
var Genre_name = map[int32]string{
0: "GENRE_UNSPECIFIED",
1: "GENRE_ROCK",
2: "GENRE_INDIE",
3: "GENRE_DRUM_AND_BASS",
}
var Genre_value = map[string]int32{
"GENRE_UNSPECIFIED": 0,
"GENRE_ROCK": 1,
"GENRE_INDIE": 2,
"GENRE_DRUM_AND_BASS": 3,
}
请注意,.proto
语言允许多个枚举符号具有相同的数值。具有相同数值的符号是同义词。这些在Go中以完全相同的方式表示,多个名称对应于相同的数值。反向映射包含单个条目,用于从数值到.proto
文件中首次出现的名称。
扩展 (proto2)
给定一个扩展定义
extend Concert {
optional int32 promo_id = 123;
}
协议缓冲区编译器将生成一个名为E_Promo_id
的protoreflect.ExtensionType
值。此值可用于proto.GetExtension
、proto.SetExtension
、proto.HasExtension
和proto.ClearExtension
函数来访问消息中的扩展。GetExtension
函数和SetExtension
函数分别返回和接受包含扩展值类型的interface{}
值。
对于单一标量扩展字段,扩展值类型是来自标量值类型表的相应Go类型。
对于单一嵌入消息扩展字段,扩展值类型为*M
,其中M
是字段消息类型。
对于重复扩展字段,扩展值类型是单一类型的切片。
例如,给定以下定义
extend Concert {
optional int32 singular_int32 = 1;
repeated bytes repeated_strings = 2;
optional Band singular_message = 3;
}
扩展值可以访问如下
m := &somepb.Concert{}
proto.SetExtension(m, extpb.E_SingularInt32, int32(1))
proto.SetExtension(m, extpb.E_RepeatedString, []string{"a", "b", "c"})
proto.SetExtension(m, extpb.E_SingularMessage, &extpb.Band{})
v1 := proto.GetExtension(m, extpb.E_SingularInt32).(int32)
v2 := proto.GetExtension(m, extpb.E_RepeatedString).([][]byte)
v3 := proto.GetExtension(m, extpb.E_SingularMessage).(*extpb.Band)
扩展可以在另一个类型内部声明。例如,一种常见的模式是这样的
message Promo {
extend Concert {
optional int32 promo_id = 124;
}
}
在这种情况下,ExtensionType
值名为E_Promo_Concert
。
服务
Go代码生成器默认情况下不会为服务生成输出。如果您启用了gRPC插件(请参阅gRPC Go快速入门指南),则将生成代码以支持gRPC。