Chapter 02

Protobuf 语法深度解析

从 .proto 文件结构到高级类型,全面掌握 Protocol Buffers 的数据定义语言

.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 类型默认值
double64 位浮点float64float0.0
float32 位浮点float32float0.0
int32有符号 32 位整数int32int0
int64有符号 64 位整数int64int0
uint32无符号 32 位uint32int0
uint64无符号 64 位uint64int0
sint32有符号 32 位,负数更高效int32int0
fixed64固定 8 字节,大数更高效uint64int0
bool布尔值boolboolfalse
stringUTF-8 字符串stringstr""
bytes任意二进制数据[]bytebytes[]byte{}

整数类型选择建议int32/int64 适用于大多数场景;如果数值经常为负数,用 sint32/sint64(ZigZag 编码,负数体积更小);如果数值通常很大(>2^28),用 fixed32/fixed64(固定字节,避免 varint 编码的低效)。

字段编号规则(极其重要!)

字段编号是 Protobuf 向后兼容性的核心机制。编号范围 1-536870911,但有几条铁则:

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 向后兼容性的关键习惯。