字节笔记本

2026年2月22日

Protobuf 生成 Go 代码指南

本教程详细描述 protocol buffer 编译器通过给定的 .proto 文件会编译生成什么 Go 代码。教程针对的是 proto3 版本的 protobuf。

编译器调用

Protobuf 核心的工具集是 C++ 语言开发的,官方的 protoc 编译器中并不支持 Go 语言,需要安装一个插件才能生成 Go 代码。用如下命令安装:

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

提供了一个 protoc-gen-go 二进制文件,当编译器调用时传递了 --go_out 命令行标志时 protoc 就会使用它。--go_out 告诉编译器把 Go 源代码写到哪里。编译器会为每个 .proto 文件生成一个单独的源代码文件。

输出文件的名称是通过获取 .proto 文件的名称并进行两处更改来计算的:

  • 生成文件的扩展名是 .pb.go。比如说 player_record.proto 编译后会得到 player_record.pb.go
  • proto 路径(使用 --proto_path-I 命令行标志指定)将替换为输出路径(使用 --go_out 标志指定)。

当你运行如下编译命令时:

bash
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 目录,但是他不能创建 build 或者 build/gen 目录,这两个必须是已经存在的目录。

如果一个 .proto 文件中有包声明,生成的源代码将会使用它来作为 Go 的包名,如果 .proto 的包名中有 . 在 Go 包名中会将 . 转换为 _。举例来说 proto 包名 example.high_score 将会生成 Go 包名 example_high_score

.proto 文件中可以使用 option go_package 指令来覆盖上面默认生成 Go 包名的规则。比如说包含如下指令的一个 .proto 文件:

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

生成的 Go 源代码的包名是 hs

如果一个 .proto 文件中不包含 package 声明,生成的源代码将会使用 .proto 文件的文件名(去掉扩展名)作为 Go 包名,. 会被首先转换为 _。举例来说一个名为 high.score.proto 不包含 pack 声明的文件将会生成文件 high.score.pb.go,他的 Go 包名是 high_score

消息

一个简单的消息声明:

protobuf
message Foo {}

protocol buffer 编译器将会生成一个名为 Foo 的结构体,实现了 proto.Message 接口的 Foo 类型的指针:

go
type Foo struct { }

// 重置 proto 为默认值
func (m *Foo) Reset() { *m = Foo{} }

// String 返回 proto 的字符串表示
func (m *Foo) String() string { return proto.CompactTextString(m) }

// ProtoMessage 作为一个 tag 确保其他人不会意外的实现 proto.Message 接口
func (*Foo) ProtoMessage() {}

内嵌的消息

一个 message 可以声明在其他 message 的内部。比如说:

protobuf
message Foo {
  message Bar { }
}

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

预定义消息类型

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

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

protobuf
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 代码将会像下面这样:

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] 包,并正常使用该类型。

字段

编译器会为每个在 message 中定义的字段生成一个 Go 结构体的字段,字段的确切性质取决于它的类型以及它是 singularrepeatedmap 还是 oneof 字段。

注意生成的 Go 结构体的字段将始终使用驼峰命名,即使在 .proto 文件中消息字段用的是小写加下划线(应该这样)。大小写转换的原理如下:

  • 首字母会大些,如果 message 中字段的第一个字符是 _,它将被替换为 X。
  • 如果内部下划线后跟小写字母,则删除下划线,并将后面跟随的字母大写。

因此,proto 字段 foo_bar_baz 在 Go 中变成 FooBarBaz_my_field_name_2 变为 XMyFieldName_2

单一标量字段

对于字段定义:

protobuf
int32 foo = 1;

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

单一 message 字段

给出如下消息类型:

protobuf
message Bar {}

对于一个有 Bar 类型字段的消息:

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

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

go
type Baz struct {
  Foo *Bar
}

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

编译器还生成一个 func (m *Baz) GetFoo() *Bar 辅助函数。这让不在中间检查 nil 值进行链式调用成为可能。

可重复字段

每个重复的字段在 Go 中的结构中生成一个 T 类型的 slice,其中 T 是字段的元素类型。对于带有重复字段的此消息:

protobuf
message Baz {
  repeated Bar foo = 1;
}

编译器会生成如下结构体:

go
type Baz struct {
  Foo []*Bar
}

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

映射字段

每个映射字段会在 Go 的结构体中生成一个 map[TKey]TValue 类型的字段,其中 TKey 是字段的键类型 TValue 是字段的值类型。对于下面这个消息定义:

protobuf
message Bar {}

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

编译器生成 Go 结构体:

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

枚举

给出如下枚举:

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

编译器将会生成一个枚举类型和一系列该类型的常量。

对于消息中的枚举(像上面那样),类型名字以消息名开头:

go
type SearchRequest_Corpus int32

对于包级别的枚举:

protobuf
// .proto
enum Foo {
  DEFAULT_BAR = 0;
  BAR_BELLS = 1;
  BAR_B_CUE = 2;
}

Go 中的类型不会对 proto 中的枚举名称进行修改:

go
type Foo int32

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

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

go
func (Foo) Enum() *Foo

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

go
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
)

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

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

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

go
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。


原文链接:https://segmentfault.com/a/1190000020418571 作者:Kevin

分享: