Go 生成代码指南 (Open)

描述 protocol buffer 编译器为任何给定的 protocol 定义所生成的 Go 代码。

proto2、proto3 和 editions 生成的代码之间的任何差异都会被强调——请注意,这些差异存在于本文档中描述的生成代码中,而不是基础 API 中,后者在两个版本中是相同的。在阅读本文档之前,您应该阅读 proto2 语言指南proto3 语言指南editions 语言指南

编译器调用

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.protobar/baz.proto,并将输出文件 foo.pb.gobar/baz.pb.go 写入 out 目录。编译器会自动创建必要的嵌套输出子目录,但不会创建输出目录本身。

包(Packages)

为了生成 Go 代码,必须为每个 .proto 文件 (包括被生成的 .proto 文件传递性依赖的文件) 提供 Go 包的导入路径。有两种方法可以指定 Go 导入路径:

  • .proto 文件中声明它,或者
  • 在调用 protoc 时在命令行上声明它。

我们建议在 .proto 文件中声明它,这样 .proto 文件的 Go 包可以与 .proto 文件本身集中识别,并简化调用 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 文件时,导入路径用于确定必须生成哪些 import 语句。例如,如果 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:新的 Opaque API 博客文章以获取介绍。

根据您的 .proto 文件使用的语法,将使用以下 API:

.proto 语法API 级别
proto2Open Struct API
proto3Open Struct API
edition 2023Open 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.Marshalproto.Size,是不安全的。

嵌套类型

消息可以声明在另一个消息内部。例如:

message Artist {
  message Name {
  }
}

在这种情况下,编译器会生成两个结构体:ArtistArtist_Name

字段

protocol buffer 编译器为消息中定义的每个字段生成一个结构体字段。该字段的确切性质取决于其类型以及它是奇异、重复、映射还是 oneof 字段。

请注意,生成的 Go 字段名始终使用驼峰命名法,即使 .proto 文件中的字段名使用带下划线的小写字母 (这是应该的)。大小写转换规则如下:

  1. 第一个字母大写以便导出。如果第一个字符是下划线,则将其删除并在前面加上一个大写 X。
  2. 如果内部下划线后跟一个小写字母,则删除下划线,并将后面的字母大写。

因此,proto 字段 birth_year 在 Go 中变为 BirthYear,而 _birth_year_2 变为 XBirthYear_2

奇异的显式存在标量字段

对于字段定义:

int32 birth_year = 1;

编译器会生成一个带有 *int32 字段 (名为 BirthYear) 的结构体,以及一个访问器方法 GetBirthYear(),该方法返回 Artist 中的 int32 值,如果字段未设置,则返回默认值。如果未显式设置默认值,则使用该类型的零值 (数字为 0,字符串为空字符串)。

对于其他标量字段类型 (包括 boolbytesstring),*int32 会根据标量值类型表替换为相应的 Go 类型。

奇异的隐式存在标量字段

对于此字段定义:

int32 birth_year = 1;

编译器将生成一个带有名为 BirthYearint32 字段的结构体,以及一个访问器方法 GetBirthYear(),该方法返回 birth_year 中的 int32 值,如果字段未设置,则返回该类型的零值 (数字为 0,字符串为空字符串)。

FirstActiveYear 结构体字段的类型将是 *int32,因为它被标记为 optional

对于其他标量字段类型 (包括 boolbytesstring),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())

重复字段

每个重复字段在 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;,编译器将生成一个 Go 结构体,其中包含一个名为 BandPromoImage[][]byte 字段。对于像 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 字段在结构体中生成一个类型为 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 中的每个奇异字段生成一个结构体。这些结构体都实现了这个 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() stringfunc (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 编译器将生成一个名为 E_Promo_idprotoreflect.ExtensionType 值。此值可与 proto.GetExtensionproto.SetExtensionproto.HasExtensionproto.ClearExtension 函数一起使用,以访问消息中的扩展。GetExtension 函数和 SetExtension 函数分别返回和接受一个包含扩展值类型的 interface{} 值。

对于奇异标量扩展字段,扩展值类型是标量值类型表中相应的 Go 类型。

对于奇异嵌入式消息扩展字段,扩展值类型为 *M,其中 M 是字段消息类型。

对于重复扩展字段,扩展值类型是奇异类型的切片。

例如,给定以下定义:

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 的代码。