Go 生成代码指南 (Open)
proto2、proto3 和 editions 生成代码之间的任何差异都会被突出显示 - 请注意,这些差异存在于本文档所述的生成代码中,而不是基础 API,基础 API 在两个版本中都是相同的。在阅读本文档之前,您应该阅读 proto2 语言指南、proto3 语言指南 或 editions 语言指南。
注意
您正在查看旧的生成代码 API(Open Struct API)的文档。有关(新)Opaque API 的相应文档,请参阅 Go 生成代码 (Opaque)。有关 Opaque API 的介绍,请参阅 Go Protobuf: The new Opaque API。编译器调用
Protocol buffer 编译器需要一个插件来生成 Go 代码。使用 Go 1.16 或更高版本,运行以下命令进行安装:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
这将会在 $GOBIN 中安装一个 protoc-gen-go 二进制文件。设置 $GOBIN 环境变量以更改安装位置。它必须在您的 $PATH 中,Protocol buffer 编译器才能找到它。
Protocol buffer 编译器在调用 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-gen-go 的特定标志是通过调用 protoc 时传递 go_opt 标志来提供的。可以传递多个 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 目录。编译器会自动创建必要的嵌套输出子目录,但不会创建输出目录本身。
包(Packages)
为了生成 Go 代码,必须为每个 .proto 文件(包括被生成文件传递依赖的那些)提供 Go 包的导入路径。有两种方法可以指定 Go 导入路径:
- 在
.proto文件中声明,或 - 在调用
protoc时在命令行中声明。
我们建议在 .proto 文件中声明,这样 .proto 文件就可以与 Go 包的导入路径集中标识,并且可以简化调用 protoc 时传递的标志集。如果某个 .proto 文件的 Go 导入路径同时由 .proto 文件本身和命令行提供,则后者优先于前者。
Go 导入路径在 .proto 文件中是本地指定的,通过声明一个 go_package 选项,并指定 Go 包的完整导入路径。例如:
option go_package = "example.com/project/protos/fizz";
Go 导入路径可以在调用编译器时在命令行中指定,通过传递一个或多个 M${PROTO_FILE}=${GO_IMPORT_PATH} 标志。例如:
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 导入路径之间也没有关联。
API 级别
生成的代码使用 Open Struct API 或 Opaque API。有关介绍,请参阅 Go Protobuf: The new Opaque API 博客文章。
根据您的 .proto 文件使用的语法,将使用以下 API:
.proto 语法 | API 级别 |
|---|---|
| proto2 | Open Struct API |
| proto3 | Open Struct API |
| edition 2023 | Open Struct API |
| edition 2024+ | Opaque API |
您可以通过在 .proto 文件中设置 api_level editions 功能来选择 API。这可以按文件或按消息设置:
edition = "2023";
package log;
import "google/protobuf/go_features.proto";
option features.(pb.go).api_level = API_OPAQUE;
message LogEntry { … }
为方便起见,您还可以使用 protoc 命令行标志覆盖默认的 API 级别:
protoc […] --go_opt=default_api_level=API_HYBRID
要为特定文件 (而不是所有文件) 覆盖默认 API 级别,请使用 apilevelM 映射标志 (类似于用于导入路径的 M 标志):
protoc […] --go_opt=apilevelMhello.proto=API_HYBRID
命令行标志也适用于仍使用 proto2 或 proto3 语法的 .proto 文件,但如果您想在 .proto 文件中选择 API 级别,则需要先将该文件迁移到 editions。
消息
给定一个简单的消息声明:
message Artist {}
Protocol buffer 编译器生成一个名为 Artist 的结构体。*Artist 实现 proto.Message 接口。
proto 包提供了操作消息的函数,包括二进制格式的转换。
proto.Message 接口定义了一个 ProtoReflect 方法。此方法返回一个 protoreflect.Message,它提供消息的基于反射的视图。
optimize_for 选项不会影响 Go 代码生成器的输出。
当多个 goroutine 同时访问同一条消息时,适用以下规则:
- 并发访问(读取)字段是安全的,但有一个例外:
- 第一次访问 延迟字段 属于修改操作。
- 修改同一消息中的不同字段是安全的。
- 并发修改字段不安全。
- 以任何方式并发修改消息以及使用
proto包的函数(如proto.Marshal或proto.Size)不安全。
嵌套类型
消息可以声明在另一个消息内部。例如:
message Artist {
message Name {
}
}
在这种情况下,编译器会生成两个结构体:Artist 和 Artist_Name。
字段
Protocol buffer 编译器为消息中定义的每个字段生成一个结构体字段。此字段的确切性质取决于其类型以及它是 singular、repeated、map 还是 oneof 字段。
请注意,生成的 Go 字段名始终使用驼峰式命名,即使 .proto 文件中的字段名使用小写带下划线(应如此)。大小写转换如下:
- 首字母大写以导出。如果第一个字符是下划线,则将其删除并预先加上大写 X。
- 如果内部下划线后跟一个小写字母,则删除下划线,并将后面的字母大写。
因此,proto 字段 birth_year 在 Go 中变为 BirthYear,而 _birth_year_2 变为 XBirthYear_2。
Singular Explicit Presence Scalar Fields
对于字段定义:
int32 birth_year = 1;
编译器将生成一个具有 *int32 字段 BirthYear 的结构体,以及一个访问器方法 GetBirthYear(),该方法返回 Artist 中的 int32 值,如果字段未设置,则返回默认值。如果未显式设置默认值,则使用该类型的 零值(数字为 0,字符串为空字符串)。
对于其他标量字段类型(包括 bool、bytes 和 string),*int32 将根据 标量值类型表替换为相应的 Go 类型。
Singular Implicit Presence Scalar Fields
对于此字段定义:
int32 birth_year = 1;
编译器将生成一个具有 int32 字段 BirthYear 的结构体,以及一个访问器方法 GetBirthYear(),该方法返回 birth_year 中的 int32 值,如果字段未设置,则返回该类型的 零值(数字为 0,字符串为空字符串)。
FirstActiveYear 结构体字段的类型将是 *int32,因为它被标记为 optional。
对于其他标量字段类型(包括 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;
}
// editions
message Concer {
Band headliner = 1;
}
编译器将生成一个 Go 结构体:
type Concert struct {
Headliner *Band
}
消息字段可以设置为 nil,这意味着该字段未设置,有效地清除了该字段。这不等同于将值设置为消息结构体的“空”实例。
编译器还生成一个 func (m *Concert) GetHeadliner() *Band 辅助函数。如果 m 为 nil 或 headliner 未设置,该函数将返回 nil *Band。这使得可以在没有中间 nil 检查的情况下链式调用 get 方法。
var m *Concert // defaults to nil
log.Infof("GetFoundingYear() = %d (no panic!)", m.GetHeadliner().GetFoundingYear())
重复字段
每个 repeated 字段会在 Go 中的结构体中生成一个 T 类型的字段,其中 T 是字段的元素类型。对于带有 repeated 字段的消息:
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;,编译器将生成一个具有 [][]byte 字段 BandPromoImage 的 Go 结构体。对于 repeated 枚举,如 repeated MusicGenre genres = 2;,编译器将生成一个具有 []MusicGenre 字段 Genre 的结构体。
以下示例展示了如何设置字段:
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 字段会在结构体中生成一个类型为 map[TKey]TValue 的字段,其中 TKey 是字段的键类型,TValue 是字段的值类型。对于带有 map 字段的消息:
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 中的每个 singular 字段生成一个结构体。这些都实现了 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)
}
编译器还生成 get 方法 func (m *Profile) GetImageUrl() string 和 func (m *Profile) GetImageData() []byte。每个 get 函数返回该字段的值,如果未设置,则返回零值。
枚举
给定一个枚举,如:
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;
// ...
}
protocol buffer 编译器将生成一个类型以及该类型的一系列常量:
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
Protocol buffer 编译器为枚举中的每个值生成一个常量。对于消息中的枚举,常量以其包含的消息名称开头:
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 文件中。
扩展
给定一个扩展定义:
extend Concert {
int32 promo_id = 123;
}
Protocol buffer 编译器将生成一个 protoreflect.ExtensionType 值,名为 E_Promo_id。此值可与 proto.GetExtension、proto.SetExtension、proto.HasExtension 和 proto.ClearExtension 函数一起使用,以访问消息中的扩展。GetExtension 函数和 SetExtension 函数分别返回和接受一个包含扩展值类型的 interface{} 值。
对于 singular 标量扩展字段,扩展值类型是 标量值类型表 中相应的 Go 类型。
对于 singular 嵌入式消息扩展字段,扩展值类型是 *M,其中 M 是字段消息类型。
对于 repeated 扩展字段,扩展值类型是 singular 类型的切片。
例如,给定以下定义:
extend Concert {
int32 singular_int32 = 1;
repeated bytes repeated_strings = 2;
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 {
int32 promo_id = 124;
}
}
在这种情况下,ExtensionType 值被命名为 E_Promo_Concert。
服务(Services)
Go 代码生成器默认不为服务生成输出。如果您启用 gRPC 插件(请参阅 gRPC Go 快速入门指南),则将生成支持 gRPC 的代码。