08-Go生成代码指南

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

0.1. 编译器调用

protocol buffers编译器需要一个插件生成Go代码。执行如下命令来安装:

go get github.com/golang/protobuf/protoc-gen-go

在命令行中使用--go_out标志时,由protoc编译器调用protoc-gen-go二进制文件。--go_out标志告诉编译器Go源文件输出位置。编译器为每个.proto文件创建单个源文件。

输出文件的名称是根据.proto文件的名称得来的,在两个地方可以更改:

  • 扩展名(.proto)替换为.pb.go。例如,名为player_record.proto的文件会生成一个名为player_record.pb.go的输出文件。
  • proto路径(使用--proto_path-I命令行标志指定)将替换为输出路径(使用--go_out标志指定)。

当像下面这样运行proto编译器时:

protoc --proto_path=src --go_out=build/gen src/foo.proto src/bar/baz.proto

编译器将读取src/foo.protosrc/bar/baz.proto文件。它产生两个输出文件:build/gen/foo.pb.gobuild/gen/bar/baz.pb.go

如有必要,编译器会自动创建build/gen/bar目录,但不会创建buildbuild/gen目录,所以这两个目录必须已经存在。

0.2. 包

如果.proto文件中有package声明,则生成的代码使用proto的package声明作为其Go包名称,其中将"."转换为"_"。例如,example.high_score的proto的package名称生成的Go包名称为example_high_score

可以使用.proto文件中的go_package选项覆盖特定.proto默认生成的包。如下.proto文件,生成的Go包名为hs

package example.high_score;
option go_package = "hs";

否则,如果.proto文件不包含package声明,则生成的代码使用文件名(减去扩展名)作为Go包名称,并将"."转换为"_“。例如,一个名为high.score.proto的proto包,其中的没有package声明,将生成在high_score包中生成一个名为high.score.pb.go的文件。

0.3. 消息

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

message Foo {}

protocol buffers 编译器生成一个名为Foo的结构和一个实现了Message*Foo的接口。有关详细信息,请参阅内联注释。

type Foo struct {
}

// Reset sets the proto's state to default values.
func (m *Foo) Reset()         { *m = Foo{} }

// String returns a string representation of the proto.
func (m *Foo) String() string { return proto.CompactTextString(m) }

// ProtoMessage acts as a tag to make sure no one accidentally implements the
// proto.Message interface.
func (*Foo) ProtoMessage()    {}

请注意,所有这些成员始终存在, optimize_for选项不会影响Go代码生成器的输出。

0.3.1. 嵌套类型

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

message Foo {
  message Bar {
  }
}

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

0.3.2. 知名类型

Protobufs 带有一组预定义的消息,称为知名类型(WKT)。这些类型可以用于与其他服务的互操作性,或者仅仅因为它们简洁地表示常见的有用模式。 例如,Struct消息表示任意C风格的结构体。

WKT的预生成Go代码作为Go protobuf库的一部分进行分发,如果使用WKT,则生成的消息的Go代码会引用此代码。例如,给出如下消息:

import "google/protobuf/struct.proto"
import "google/protobuf/timestamp.proto"

message NamedStruct {
  string name = 1;
  google.protobuf.Struct definition = 2;
  google.protobuf.Timestamp last_modified = 3;
}

生成的Go代码如下所示:

import google_protobuf "github.com/golang/protobuf/ptypes/struct"
import google_protobuf1 "github.com/golang/protobuf/ptypes/timestamp"

...

type NamedStruct struct {
   Name         string
   Definition   *google_protobuf.Struct
   LastModified *google_protobuf1.Timestamp
}

一般来说,不需要将这些类型直接导入代码中。但是,如果需要直接引用其中一种类型,只需导入github.com/golang/protobuf/ptypes/[TYPE]包,并正常使用该类型。

0.4. 字段

protocol buffers编译器为消息中定义的每个字段生成结构体的字段。该字段的确切性质取决于它的类型以及它是否是singular, repeated, map或者oneof字段。

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

  • 第一个字母是导入的。如果第一个字符是下划线,则将其删除并添加大写字母X
  • 如果内部下划线后跟小写字母,则删除下划线,并将后面的字母大写。

因此:

  • 原字段foo_bar_baz在Go中变为FooBarBaz
  • 原字段_my_field_name_2在GO中变为XMyFieldName_2

0.4.1. 单个标量字段 (proto2)

对于以下任一字段定义:

optional int32 foo = 1;
required int32 foo = 1;

编译器生成一个结构,其中包含一个名为Foo*int32字段和一个访问器方法GetFoo()它返回Foo中的int32值或默认值(如果该字段未设置)。如果未显式设置默认值,则使用该类型的零值(0表示数字,空字符串表示字符串)。

对于其他标量字段类型(包括boolbytesstring),*int32将根据[标量值类型]..(/Protocol-Buffers/02-proto3指南.md)表替换为相应的Go类型。

0.4.2. 单个标量字段 (proto3)

对于此字段定义:

int32 foo = 1;

编译器将生成一个带有名为Fooint32字段和一个访问器方法GetFoo()的结构体,该方法返回Foo中的int32值或该字段的零值,如果字段未设置(0表示数字,字符串为空字符串)。

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

0.4.3. 单个消息字段

给定消息类型:

message Bar {}

对于带有Bar字段的消息:

// proto2
message Baz {
  optional Bar foo = 1;
  // The generated code is the same result if required instead of optional.
}

// proto3
message Baz {
  Bar foo = 1;
}

编译器将生成Go结构:

type Baz struct {
        Foo *Bar
}

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

编译器还生成一个func(m *Baz) GetFoo() *Bar辅助函数。这使得可以在没有中间nil检查的情况下链接获取调用。

0.4.4. Repeated字段

每个repeated字段生成一个T字段的切片(slice)在Go结构中,其中T是字段的元素类型。如下消息带有repeated字段:

message Baz {
  repeated Bar foo = 1;
}

编译器生成Go结构:

type Baz struct {
        Foo  []*Bar
}

同样:

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

0.4.5. Map字段

每个map字段以类型map[TKey]TValue在结构体中生成一个字段,其中TKey是字段的键类型,TValue是字段的值类型。如下消息带有map字段:

message Bar {}

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

编译器生成Go结构:

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

0.4.6. 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{"http://example.com/image.png"},
}

// imageData is []byte
imageData := getImageData()
p2 := &account.Profile{
  Avatar: &account.Profile_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函数返回该字段的值,如果未设置则返回零值。

0.5. 枚举

给出如下枚举:

message SearchRequest {
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 1;
  ...
}

protocol buffers编译器生成一个类型和一系列该类型的常量

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

type SearchRequest_Corpus int32

对于包级别的枚举:

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

Go生成的类型名称从未修改的proto枚举名称得来:

type Foo int32

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

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

func (Foo) Enum() *Foo

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

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
)

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

const (
        Foo_DEFAULT_BAR Foo = 0
        Foo_BAR_BELLS   Foo = 1
        Foo_BAR_B_CUE   Foo = 2
)

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

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文件中。

0.6. 扩展(proto2)

扩展仅存在于proto2中。有关proto2扩展的Go生成代码API的文档,请参阅proto包doc

0.7. 服务

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

上次修改: 14 April 2020