本页确切描述了protocol buffer编译器为任何给定的协议定义生成的Go代码。突出显示了proto2和proto3生成的代码之间的任何差异-请注意,这些差异在本文档中描述的生成的代码中,而不是在基本API中,这在两个版本中都是相同的。在阅读本文档之前,您应该阅读 proto2语言指南和/或 proto3语言指南。

编译器调用

protocol buffer编译器需要一个插件来生成Go代码。通过运行以下命令使用Go 1.16或更高版本进行安装:

1
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

这将在$GOBIN中安装protoc-gen-go二进制文件。设置$GOBIN环境变量以更改安装位置。它必须是在你$PATH的协议缓冲编译器找到它。

proto编译器使用该go_out标志调用时,会生成Go输出。go_out标志的参数是您希望编译器在其中写入Go输出的目录。编译器为每个.proto文件输入创建一个源文件。通过将.proto扩展名替换为来创建输出文件的名称.pb.go。

生成的.pb.go文件在输出目录中的放置位置取决于编译器标志。有几种输出模式:

  • 如果指定了path = import标志,则将输出文件放置在以Go软件包的导入路径命名的目录中。 例如,输入文件protos / buzz.proto的go导入路径为example.com/project/protos/fizz,则输出文件为example.com/project/protos/fizz/buzz.pb.go。 如果未指定路径标志,则这是默认的输出模式。
  • 如果指定了module = $ PREFIX标志,则将输出文件放置在以Go软件包的导入路径命名的目录中,但将从输出文件名中删除指定的目录前缀。 例如,将Go导入路径为example.com/project/protos/fizz和example.com/project指定为模块前缀的输入文件protos / buzz.proto会在protos / fizz / buzz.pb中生成输出文件.go。 在模块路径之外生成任何Go软件包都会导致错误。 此模式对于将生成的文件直接输出到Go模块很有用。
  • 如果指定了paths = source_relative标志,则将输出文件放置在与输入文件相同的相对目录中。例如,输入文件protos / buzz.proto导致输出文件为protos / buzz.pb.go。

protoc-gen-go通过go_opt调用时传递标志来提供特定于的标志protoc。go_opt可以传递多个标志。例如,运行时:

1
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包的导入路径(包括.proto正在生成的文件所依赖的文件)。有两种方法可以指定Go导入路径:

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

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

.proto通过go_package在Go包的完整导入路径中声明一个选项,可以在文件中本地指定Go导入路径。用法示例:

1
option go_package = "example.com/project/protos/fizz";

可以在调用编译器时通过传递一个或多个M${PROTO_FILE}=${GO_IMPORT_PATH}标志的方式在命令行上指定Go导入路径。用法示例:

1
2
3
4
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.protoimports b.proto,则生成的a.pb.go文件需要导入包含生成的b.pb.go文件的Go包(除非两个文件都在同一个包中)。导入路径还用于构造输出文件名。有关详细信息,请参见上面的“编译器调用”部分。

Go导入路径与.proto文件中的包说明符之间没有任何关联。 后者仅与protobuf命名空间有关,而前者仅与Go命名空间有关。 此外,Go导入路径和.proto导入路径之间没有关联。

Messages

给定一个简单的消息声明:

1
message Foo {}

proto buffer’s编译器生成一个称为Foo的结构。 * Foo实现了proto.Message接口。

该 proto软件包 提供了对消息进行操作的功能,包括与二进制格式之间的转换。

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

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

嵌套类型

一条消息可以在另一条消息中声明。例如:

1
2
3
4
message Foo {
  message Bar {
  }
}

在这种情况下,编译器会生成两个结构:Foo和 Foo_Bar。

Fields

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

请注意,即使.proto文件中的字段名称使用带下划线的小写字母(应如此),生成的Go字段名称也始终使用驼峰式命名。大小写转换的工作方式如下:

  1. 第一个字母大写用于导出。如果第一个字符是下划线,则将其删除并以大写字母X开头。
  2. 如果内部下划线后跟小写字母,则下划线将被删除,并且以下字母大写。

因此,原始字段foo_bar_baz变为 FooBarBazGo,_my_field_name_2变为 XMyFieldName_2。

单独常量字段(proto3)

对于此字段定义:

1
int32 foo = 1;

编译器将生成一个结构,该结构具有一个int32名为的字段 Foo和一个访问器方法GetFoo(),如果未设置该字段,则返回该类型的 int32值Foo或 零值(0对于数字,对于字符串为空字符串)。

对于其他标量字段类型(包括bool, bytes和string),int32根据标量值类型表将其替换为相应的Go类型 。原型中未设置的值将表示为该类型的 零值(0对于数字,对于字符串,为空字符串)。

单独消息字段

给定消息类型:

1
message Bar {}

对于带有Bar字段的消息:

1
2
3
4
// proto3
message Baz {
  Bar foo = 1;
}

编译器将生成一个Go结构

1
2
3
type Baz struct {
        Foo *Bar
}

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

编译器还会生成一个func (m *Baz) GetFoo()*Bar 辅助函数。如果m为nil或未foo设置,此函数返回nil *Bar。这样就可以在没有中间nil检查的情况下链式调用获取。

重复字段

每个重复的字段T都会在Go的结构中生成一个字段切片,该字段的元素类型为T。对于具有重复字段的此消息:

1
2
3
message Baz {
  repeated Bar foo = 1;
}

编译器生成Go结构:

1
2
3
type Baz struct {
        Foo  []*Bar
}

同样,对于字段定义repeated bytes foo = 1;,编译器将使用名为[][]byte的字段 生成Go结构Foo。对于重复枚举 repeated MyEnum bar = 2;,编译器生成一个结构,该结构带有一个名为Bar的[]MyEnum

以下示例显示如何设置字段:

1
2
3
4
5
6
baz := &Baz{
  Foo: []*Bar{
    {}, // First element.
    {}, // Second element.
  },
}

要访问该字段,您可以执行以下操作:

1
2
foo := baz.GetFoo() // foo type is []*Bar.
b1 := foo[0] // b1 type is *Bar, the first element in foo.

Map

每个映射字段都会在类型的结构中生成一个字段, map[TKey]TValue其中,TKey是字段的键类型和TValue字段的值类型。对于带有地图字段的此消息:

1
2
3
4
5
message Bar {}

message Baz {
  map<string, Bar> foo = 1;
}

编译器生成Go结构:

1
2
3
type Baz struct {
        Foo map[string]*Bar
}

oneof

对于一个oneof字段,protobuf编译器会生成一个具有接口type的单个字段isMessageName_MyField。它还为其中一个中的每个单独字段生成一个结构。这些都实现此isMessageName_MyField接口。

对于带有oneof字段的此消息:

1
2
3
4
5
6
7
package account;
message Profile {
  oneof avatar {
    string image_url = 1;
    bytes image_data = 2;
  }
}

编译器生成以下结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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()方法。

以下示例显示如何设置字段:

1
2
3
4
5
6
7
8
9
p1 := &account.Profile{
  Avatar: &account.Profile_ImageUrl{"http://example.com/image.png"},
}

// imageData is []byte
imageData := getImageData()
p2 := &account.Profile{
  Avatar: &account.Profile_ImageData{imageData},
}

要访问该字段,可以使用值上的类型开关来处理不同的消息类型。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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函数都返回该字段的值,如果未设置,则返回零值。

枚举

给定一个像这样的枚举:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
message SearchRequest {
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 1;
  ...
}

protocol buffer编译器会生成一个类型和一系列带有该类型的常量。

对于消息中的枚举(如上面的枚举),类型名称以消息名称开头:

1
type SearchRequest_Corpus int32

对于包级枚举:

1
2
3
4
5
enum Foo {
  DEFAULT_BAR = 0;
  BAR_BELLS = 1;
  BAR_B_CUE = 2;
}

Go类型名称不对原始枚举名称进行修改:

1
type Foo int32

此类型具有一种String()返回给定值名称的方法。

该Enum()方法使用给定值初始化新分配的内存,并返回相应的指针:

1
func (Foo) Enum() *Foo

protocol buffer编译器会为枚举中的每个值生成一个常量。对于消息中的枚举,常量以包含消息的名称开头:

1
2
3
4
5
6
7
8
9
const (
        SearchRequest_UNIVERSAL SearchRequest_Corpus = 0
        SearchRequest_WEB       SearchRequest_Corpus = 1
        SearchRequest_IMAGES    SearchRequest_Corpus = 2
        SearchRequest_LOCAL     SearchRequest_Corpus = 3
        SearchRequest_NEWS      SearchRequest_Corpus = 4
        SearchRequest_PRODUCTS  SearchRequest_Corpus = 5
        SearchRequest_VIDEO     SearchRequest_Corpus = 6
)

对于包级枚举,常量以枚举名称开头:

1
2
3
4
5
const (
        Foo_DEFAULT_BAR Foo = 0
        Foo_BAR_BELLS   Foo = 1
        Foo_BAR_B_CUE   Foo = 2
)

protobuf编译器还会生成一个从整数值到字符串名称的映射以及从名称到值的映射:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var Foo_name = map[int32]string{
        0: "DEFAULT_BAR",
        1: "BAR_BELLS",
        2: "BAR_B_CUE",
}
var Foo_value = map[string]int32{
        "DEFAULT_BAR": 0,
        "BAR_BELLS":   1,
        "BAR_B_CUE":   2,
}

请注意,.proto语言允许多个枚举符号具有相同的数值。 具有相同数值的符号是同义词。 这些在Go中以完全相同的方式表示,具有对应于相同数值的多个名称。 反向映射包含一个将数字值输入到.proto文件中最先出现的名称的条目。

服务

默认情况下,Go代码生成器不会为服务生成输出。如果启用了gRPC插件(请参阅 gRPC Go快速入门指南),那么将生成支持gRPC的代码。