Go 生成代码指南(开放式)

精确描述了协议缓冲区编译器针对任何给定的协议定义生成的 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-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 目录。编译器在必要时会自动创建嵌套输出子目录,但不会创建输出目录本身。

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

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

我们建议在 .proto 文件中声明它,以便 .proto 文件的 Go 包可以与 .proto 文件本身集中识别,并简化调用 protoc 时传递的标志集。如果给定 .proto 文件的 Go 导入路径同时由 .proto 文件本身和命令行提供,则后者优先于前者。

Go 导入路径是通过在 .proto 文件中声明一个带有 Go 包完整导入路径的 go_package 选项来局部指定的。示例用法

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 级别

生成的代码使用开放结构 API 或不透明 API。请参阅 Go Protobuf:新的不透明 API 博客文章以了解介绍。

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

.proto 语法API 级别
proto2开放结构 API
proto3开放结构 API
版本 2023开放结构 API
版本 2024+不透明 API

您可以通过在 .proto 文件中设置 api_level 版本特性来选择 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 {}

协议缓冲区编译器会生成一个名为 Artist 的结构体。一个 *Artist 实现了 proto.Message 接口。

proto提供了操作消息的函数,包括二进制格式的转换。

proto.Message 接口定义了一个 ProtoReflect 方法。此方法返回一个 protoreflect.Message,它提供了基于反射的消息视图。

optimize_for 选项不影响 Go 代码生成器的输出。

当多个 goroutine 并发访问同一消息时,适用以下规则

  • 并发访问(读取)字段是安全的,有一个例外
  • 修改同一消息中不同的字段是安全的。
  • 并发修改同一字段是不安全的。
  • proto中的函数(例如 proto.Marshalproto.Size)并发地以任何方式修改消息是不安全的。

嵌套类型

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

message Artist {
  message Name {
  }
}

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

字段

协议缓冲区编译器为消息中定义的每个字段生成一个结构体字段。此字段的具体性质取决于其类型以及它是单一字段、重复字段、map 字段还是 oneof 字段。

请注意,生成的 Go 字段名始终使用驼峰命名法 (camel-case naming),即使 .proto 文件中的字段名使用小写加下划线(正如应该的那样)。大小写转换如下进行

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

因此,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,字符串为空字符串)。

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

单一标量字段 (proto3)

对于此字段定义

int32 birth_year = 1;
optional int32 first_active_year = 2;

编译器将生成一个结构体,其中包含一个名为 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;
}

编译器将生成一个 Go 结构体

type Concert struct {
    Headliner *Band
}

消息字段可以设置为 nil,这意味着该字段未设置,从而有效地清除了该字段。这与将值设置为消息结构体的“空”实例不同。

编译器还会生成一个辅助函数 func (m *Concert) GetHeadliner() *Band。如果 m 为 nil 或 headliner 未设置,此函数将返回 nil *Band。这使得链式调用 get 方法成为可能,而无需进行中间的 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;,编译器将生成一个包含 [][]byte 字段(名为 BandPromoImage)的 Go 结构体。对于像 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 字段在 Go 结构体中生成一个类型为 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;
  // ...
}

协议缓冲区编译器会生成一个类型和一系列具有该类型的常量

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 编译器还会生成一个从整数值到字符串名称的 map,以及一个从名称到值的 map

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_idprotoreflect.ExtensionType 值。该值可与 proto.GetExtensionproto.SetExtensionproto.HasExtensionproto.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。