.proto 文件结构
每个 .proto 文件的顶部必须声明语法版本。推荐使用 proto3(proto2 已较少使用)。
// user.proto
syntax = "proto3";
// package 避免不同服务的命名冲突
package user.v1;
// Go 代码生成到哪个包
option go_package = "github.com/myapp/gen/user/v1;userv1";
// Python 无 go_package,但可设置 Java 包
option java_package = "com.myapp.user.v1";
// 导入其他 proto 文件
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
message 定义与标量类型
message 是 Protobuf 的核心数据结构,类似于其他语言中的 class 或 struct。每个字段由 类型、名称、字段编号 三部分组成。
message User {
// 字段格式:类型 名称 = 字段编号;
int64 id = 1;
string name = 2;
string email = 3;
bool is_active = 4;
double balance = 5;
bytes avatar = 6; // 原始二进制数据
}
标量类型完整对照表
| Protobuf 类型 | 说明 | Go 类型 | Python 类型 | 默认值 |
|---|---|---|---|---|
double | 64 位浮点 | float64 | float | 0.0 |
float | 32 位浮点 | float32 | float | 0.0 |
int32 | 有符号 32 位整数 | int32 | int | 0 |
int64 | 有符号 64 位整数 | int64 | int | 0 |
uint32 | 无符号 32 位 | uint32 | int | 0 |
uint64 | 无符号 64 位 | uint64 | int | 0 |
sint32 | 有符号 32 位,负数更高效 | int32 | int | 0 |
fixed64 | 固定 8 字节,大数更高效 | uint64 | int | 0 |
bool | 布尔值 | bool | bool | false |
string | UTF-8 字符串 | string | str | "" |
bytes | 任意二进制数据 | []byte | bytes | []byte{} |
整数类型选择建议:int32/int64 适用于大多数场景;如果数值经常为负数,用 sint32/sint64(ZigZag 编码,负数体积更小);如果数值通常很大(>2^28),用 fixed32/fixed64(固定字节,避免 varint 编码的低效)。
字段编号规则(极其重要!)
字段编号是 Protobuf 向后兼容性的核心机制。编号范围 1-536870911,但有几条铁则:
- 1-15 编码为 1 字节,优先分配给高频字段(如 id、name)。一旦分配不要更改。
- 16-2047 编码为 2 字节,用于次高频字段。
- 19000-19999 Protobuf 内部保留,不可使用。
-
不可重用编号
删除字段后,该编号必须用
reserved保留,防止他人复用造成数据混乱。 - 不可修改编号 字段编号确定后永远不要修改,Protobuf 序列化只认编号,不认字段名。
message User {
int64 id = 1;
string name = 2;
// string old_email = 3; // 已删除的字段
// 保留已删除字段的编号和名称,防止误用
reserved 3, 4, 10 to 15;
reserved "old_email", "old_phone";
string email = 5; // 新字段从未用过的编号开始
}
enum 枚举
enum UserStatus {
// proto3 要求第一个枚举值必须是 0(默认值)
USER_STATUS_UNSPECIFIED = 0;
USER_STATUS_ACTIVE = 1;
USER_STATUS_INACTIVE = 2;
USER_STATUS_BANNED = 3;
}
message User {
int64 id = 1;
string name = 2;
UserStatus status = 3; // 使用枚举类型
}
枚举命名惯例:枚举值名称前缀与枚举类型名一致(如 USER_STATUS_ACTIVE)。第一个值必须是 0 值,且通常命名为 _UNSPECIFIED,代表"未设置/未知"状态,这是 proto3 的规范,也有助于向后兼容。
嵌套 message
message Address {
string street = 1;
string city = 2;
string country = 3;
}
message User {
int64 id = 1;
string name = 2;
Address address = 3; // 嵌套消息
// 也可以在 message 内部定义嵌套类型
message Preferences {
bool email_notifications = 1;
string language = 2;
}
Preferences preferences = 4;
}
repeated(数组)
repeated 关键字表示一个可重复的字段,对应代码中的数组/切片/列表。
message User {
int64 id = 1;
string name = 2;
repeated string tags = 3; // []string in Go
repeated Address addresses = 4; // []*Address in Go
}
oneof(互斥字段)
oneof 表示一组互斥字段,同一时刻只有一个字段有值。常用于联合类型(Union Type)场景。
message Notification {
string title = 1;
// payload 只能是 email、sms、push 之一
oneof payload {
EmailPayload email = 2;
SmsPayload sms = 3;
PushPayload push = 4;
}
}
message EmailPayload {
string to = 1;
string subject = 2;
string body = 3;
}
map(键值映射)
message Configuration {
// map<key_type, value_type> field_name = N;
// key_type 必须是整数或 string
map<string, string> labels = 1;
map<string, int32> counters = 2;
map<int32, Feature> features = 3;
}
// 注意:map 字段不能是 repeated
// map 内部已经是无序集合,不保证迭代顺序
Well-Known Types(预定义类型)
Google 提供了一批预定义的常用类型,位于 google/protobuf/ 下,可直接 import 使用。
import "google/protobuf/timestamp.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/any.proto";
import "google/protobuf/wrappers.proto";
import "google/protobuf/empty.proto";
message Event {
string id = 1;
google.protobuf.Timestamp created_at = 2; // 时间戳
google.protobuf.Duration ttl = 3; // 时间段
google.protobuf.Any payload = 4; // 任意类型
google.protobuf.StringValue nickname = 5; // 可空 string
}
// google.protobuf.Empty 用于无参数或无返回值的 RPC
rpc HealthCheck(google.protobuf.Empty) returns (google.protobuf.Empty);
| Well-Known Type | 用途 | Go 对应类型 |
|---|---|---|
Timestamp | 时间点(Unix 纳秒) | time.Time(via timestamppb) |
Duration | 时间段 | time.Duration(via durationpb) |
Any | 任意消息类型(类型擦除) | *anypb.Any |
StringValue | 可空字符串(区分空串与未设置) | *wrapperspb.StringValue |
Int64Value | 可空整数 | *wrapperspb.Int64Value |
BoolValue | 可空布尔 | *wrapperspb.BoolValue |
Empty | 空消息(无参数/无返回) | *emptypb.Empty |
Struct | 动态 JSON 对象 | *structpb.Struct |
为什么需要 Wrapper 类型?:proto3 的标量字段无法区分"未设置"与"设置为零值"(0、false、"")。使用 google.protobuf.StringValue 等 Wrapper 类型,可以区分 null(未设置)与 ""(空字符串),对可选字段非常重要。
完整示例:电商订单消息
syntax = "proto3";
package order.v1;
option go_package = "github.com/myapp/gen/order/v1;orderv1";
import "google/protobuf/timestamp.proto";
enum OrderStatus {
ORDER_STATUS_UNSPECIFIED = 0;
ORDER_STATUS_PENDING = 1;
ORDER_STATUS_PAID = 2;
ORDER_STATUS_SHIPPED = 3;
ORDER_STATUS_COMPLETED = 4;
ORDER_STATUS_CANCELLED = 5;
}
message OrderItem {
string product_id = 1;
string product_name = 2;
int32 quantity = 3;
double unit_price = 4;
}
message Order {
string id = 1;
string user_id = 2;
OrderStatus status = 3;
repeated OrderItem items = 4;
double total = 5;
map<string, string> metadata = 6;
google.protobuf.Timestamp created_at = 7;
google.protobuf.Timestamp updated_at = 8;
}
本章小结:Protobuf 的语法简洁但规则严格,其中最重要的是字段编号——它是序列化的唯一标识,一旦发布就不可修改。合理使用 reserved 保护已删除字段,是维护 API 向后兼容性的关键习惯。