Web

在 Go 中使用 Protocol Buffers

API 服务中通常会用 .proto 定义请求、响应和用户信息等数据结构,再由 protoc-gen-go 生成对应的 .pb.go。本文用一个泛化示例介绍 Protocol Buffers 的常见用法,不涉及 gRPC 服务等高级内容。

示例目录

text
api/v1/
├── user/
│   ├── user.proto
│   └── user.pb.go
├── order/
│   ├── order.proto
│   └── order.pb.go
└── common/
    ├── protocol.proto
    └── protocol.pb.go

每个 .proto 文件旁边都有一份由 protoc-gen-go 生成的 .pb.go。生成文件用于 Go 代码编译,不应手动修改。

定义消息

proto
syntax = "proto3";

package user.v1;

option go_package = "example.com/demo/api/v1/user;userpb";

message UserProfile {
  int64 user_id = 1;       // @gotags: json:"userId"
  string avatar = 2;
  string username = 3;     // @gotags: json:"username"
  string name = 4;
  string created_at = 5;   // @gotags: json:"createdAt"
  bool verified = 6;
}

这里包含几个常见部分:

  • syntax = "proto3" 声明使用 proto3;
  • package user.v1 是 Protobuf 命名空间;
  • option go_package 指定生成代码的 Go 导入路径和包名;
  • message 定义一条消息;
  • int64stringbool 是字段类型;
  • 字段后的数字是字段编号,用于二进制编码,不能随意修改;
  • 注释中的 @gotags 可以控制生成结构体上的 JSON tag。

go_package

go_package 的格式是 "导入路径;包名"。例如:

proto
option go_package = "example.com/demo/api/v1/user;userpb";

也可以写完整模块路径:

proto
option go_package = "github.com/yourorg/yourproject/api/v1/common/protocol;protocolpb";

分号前是 Go 导入路径,分号后是生成的 Go 包名。例如上面的 user 文件生成后是:

go
package userpb

导入时需要根据项目 go.mod 拼出完整导入路径。若模块名是 example.com/demo,则导入路径为:

go
import userpb "example.com/demo/api/v1/user"

定义枚举

proto
package common.v1;

option go_package = "example.com/demo/api/v1/common/protocol;protocolpb";

enum Protocol {
  UNSPECIFIED = 0;
  OAUTH2 = 1;
  OIDC = 2;
  SAML2 = 3;
  TRUSTED = 4;
}

proto3 枚举的第一个值必须是 0。生成后得到 Protocol 类型和对应常量,例如 Protocol_OAUTH2

嵌套消息

消息内部可以再定义消息。下面的响应包含一个嵌套的 Profile

proto
message UserInfoResponse {
  message Profile {
    string bio = 1;
    string website = 2;
    string location = 3;
  }

  string username = 1;
  string nickname = 2;
  string avatar = 3;

  Profile profile = 4;
}

嵌套消息在 Go 中会生成类似 UserInfoResponse_Profile 的类型。

导入其他 proto

一个 .proto 文件可以导入同仓库的另一个 .proto,也可以导入 Protobuf 提供的标准类型:

proto
import "api/v1/common/protocol.proto";
import "google/protobuf/struct.proto";

导入后可以在消息中使用对应类型:

proto
message AuthResult {
  string state = 1;
  common.v1.Protocol protocol = 2;
  google.protobuf.Struct raw_user = 3;
}

这里的 common.v1.Protocol 是另一个 proto 文件中的枚举,google.protobuf.Struct 表示任意 JSON 对象。

optional 字段

消息中的字段可以使用 optional

proto
optional bool bound = 7;

optional 表示这个字段可以不存在。生成到 Go 后通常是指针类型 *bool,这样可以区分“没有传”和“显式传了 false”。

生成后的 Go 类型

UserProfile 为例,生成结构体可以简化为:

go
type UserProfile struct {
	UserId    int64  `json:"userId"`
	Avatar    string `json:"avatar,omitempty"`
	Username  string `json:"username"`
	Name      string `json:"name,omitempty"`
	CreatedAt string `json:"createdAt"`
	Verified  bool   `json:"verified,omitempty"`
}

实际生成文件还会包含 ProtoReflect() 等 Protobuf 运行时需要的方法。生成代码由工具维护,不要手动修改。

使用生成类型

在 Go 代码中可以直接创建生成的类型:

go
import (
	userpb "example.com/demo/api/v1/user"
)

profile := &userpb.UserProfile{
	UserId:   1,
	Username: "alice",
}

总结

上面的泛化示例覆盖了 Protocol Buffers 在 Go 中的常见写法:用 message 定义数据结构,用 enum 定义枚举,用 import 复用其他 proto,用 optional 表达可缺省字段,并用 go_package 控制生成的 Go 导入路径和包名。