Protobuf 语法基础:消息类型定义、字段编号规则、字段类型、默认值
好,咱们开始聊 Protobuf 的消息定义。
说实话,我第一次写 .proto 文件的时候,觉得这不就是个结构体嘛,随便写写就行。后来线上出了兼容性问题,才意识到里面门道不少。这一节我把几个最基础也最容易踩坑的点捋清楚。
消息类型定义
Protobuf 里用 message 关键字来定义一个消息类型。你可以把它想象成一张数据表格的 schema,但比 JSON 严格得多。
message UserInfo {
string user_id = 1;
string nickname = 2;
int32 age = 3;
repeated string tags = 4;
}
每个消息就是一个逻辑单位,gRPC 的服务接口里,请求和响应都靠 message 包装。我习惯一个 .proto 文件里放一组相关的 message,比如 user.proto 里放 UserInfo、LoginRequest、LoginResponse,别把订单和用户杂糅在一起——你想想看,项目大了光翻文件就够呛。
字段编号规则
每个字段必须有一个唯一的编号,这个编号在二进制编码中就是字段的身份标识,不是字段顺序。字段编号一旦被使用,尽量不要改,哪怕你把那个字段删了,编号也要“占住”,用 reserved 声明。
编号范围:
- 1 ~ 15:占用 1 个字节,用于最常用的字段
- 16 ~ 2047:占用 2 个字节
- 2048 ~ 536870911:占用 3~5 个字节(你基本用不到这么大)
我早期犯过一个错:把频繁出现的 id 字段用了 16 号,结果每个 message 都多花一个字节,积少成多几百兆流量就没了。所以高频字段尽量用 1~15。
还有一点:字段编号不要超过 2^29 - 1,也不能用负数。如果以后要删字段,用 reserved 标记,防止别人误用。
message UserInfo {
reserved 2, 15, 20 to 30; // 这些编号以后不能再用
string user_id = 1;
// nickname 已删除,编号 2 被保留
int32 age = 3;
}
字段类型
Protobuf 给了一套标量类型,跟各种语言的映射关系你得心里有数。我整理了一个常用对照表:
| .proto 类型 | C++ / Go / Python 对应 | 打包字节 | 注意事项 |
|---|---|---|---|
| double | double / float64 / float | 8 | 高精度场景用 |
| float | float / float32 / float | 4 | |
| int32 | int32 / int32 / int | 变长(1~5) | 负数值效率低,建议用 sint32 |
| int64 | int64 / int64 / int | 变长(1~10) | 同理 |
| uint32 | uint32 / uint32 / int | 变长 | 无符号 |
| uint64 | uint64 / uint64 / int | 变长 | |
| sint32 | int32 / int32 / int | 变长 | 用 ZigZag 编码,负数友好 |
| fixed32 | uint32 / uint32 / int | 4 | 固定长度,编码快 |
| fixed64 | uint64 / uint64 / int | 8 | |
| bool | bool / bool / bool | 1 | |
| string | string / string / str | UTF-8 长度+内容 | 必须 UTF-8 |
| bytes | string / []byte / bytes | 长度+内容 | 任意二进制 |
另外,repeated 可以定义列表字段,map 可以定义键值对。还有枚举(enum)和 oneof,咱们后面章节再细聊。
默认值
Protobuf 对每个标量类型都有隐式默认值。序列化时,如果字段的值等于默认值,默认是不写入字节流的——这是为了节省空间。
- 字符串:默认是空字符串
"" - 数字类型:默认是
0 - 布尔:默认是
false - bytes:默认是空字节串
- 枚举:默认是第一个枚举值(必须是 0)
- repeated:默认是空列表
- map:默认是空 map
这里有个容易踩的坑:你从解析出来的 message 里拿一个字段,如果对方没设置,你就拿到了默认值。但你怎么区分“对方没设置”和“对方故意设了默认值”?比如年龄是 0,可能对方没发这个字段,也可能用户年龄真的是 0。我们在业务里通常用 optional 配合 google.protobuf.wrapper 类型来解决,或者用 oneof 包裹一个 has 标志。
本章知识结构图
嗯,到这就算把 proto 消息的骨架搭起来了。记住字段编号是命根子,默认值是双刃剑,类型选择影响性能。下节课我们聊枚举和 oneof,那时候你会更理解为什么每个细节都值得推敲。