在 Go 中使用 Protocol Buffers
API 服务中通常会用 .proto 定义请求、响应和用户信息等数据结构,再由 protoc-gen-go 生成对应的 .pb.go。本文用一个泛化示例介绍 Protocol Buffers 的常见用法,不涉及 gRPC 服务等高级内容。
示例目录
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 代码编译,不应手动修改。
定义消息
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定义一条消息;int64、string、bool是字段类型;- 字段后的数字是字段编号,用于二进制编码,不能随意修改;
- 注释中的
@gotags可以控制生成结构体上的 JSON tag。
go_package
go_package 的格式是 "导入路径;包名"。例如:
option go_package = "example.com/demo/api/v1/user;userpb";也可以写完整模块路径:
option go_package = "github.com/yourorg/yourproject/api/v1/common/protocol;protocolpb";分号前是 Go 导入路径,分号后是生成的 Go 包名。例如上面的 user 文件生成后是:
package userpb导入时需要根据项目 go.mod 拼出完整导入路径。若模块名是 example.com/demo,则导入路径为:
import userpb "example.com/demo/api/v1/user"定义枚举
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:
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 提供的标准类型:
import "api/v1/common/protocol.proto";
import "google/protobuf/struct.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:
optional bool bound = 7;optional 表示这个字段可以不存在。生成到 Go 后通常是指针类型 *bool,这样可以区分“没有传”和“显式传了 false”。
生成后的 Go 类型
以 UserProfile 为例,生成结构体可以简化为:
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 代码中可以直接创建生成的类型:
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 导入路径和包名。