reserved 关键字
reserved 用于保留字段编号或字段名,防止未来不小心重用已删除的字段。这对于维护向后兼容性非常重要。
使用场景:
- 当你需要删除某个字段时,应该使用
reserved标记该字段编号,而不是直接删除 - 防止未来的开发者重用已删除字段的编号,导致数据解析错误
- 保证不同版本的 proto 文件之间的兼容性
语法示例:
message User {
// 保留单个字段编号
reserved 2;
// 保留多个字段编号
reserved 4, 5, 6;
// 保留字段编号范围
reserved 8 to 10;
// 保留字段名(防止字段名被重用)
reserved "old_field", "deprecated_name";
int64 id = 1;
string username = 3;
string email = 7;
int64 created_at = 11;
}
注意事项:
- 不能在同一个
reserved语句中混合使用字段编号和字段名,需要分开写 - 字段编号保留后,不能再用于新字段
- 字段名保留后,不能再用于新字段
实际示例:
假设最初的 proto 定义:
message User {
int64 id = 1;
string username = 2;
string password = 3; // 敏感字段,需要删除
string email = 4;
}
删除 password 字段后的正确做法:
message User {
reserved 3; // 保留编号 3,防止未来误用
reserved "password"; // 保留字段名
int64 id = 1;
string username = 2;
string email = 4;
}
optional 关键字(可选参数)
在 Protocol Buffers 中,proto3 和 proto2 对字段的处理方式不同:
proto2:
- 支持
required、optional、repeated三种修饰符 required:必填字段,如果未设置会导致序列化失败optional:可选字段,可以检测字段是否被设置
proto3:
- 默认所有字段都是可选的(除了
repeated) - 移除了
required关键字(避免兼容性问题) - 从 proto3.15 开始,重新引入了
optional关键字,用于区分"未设置"和"设置为默认值"
proto3 中的 optional
在 proto3 中,默认字段无法区分"未设置"和"设置为零值"。例如:
syntax = "proto3";
message User {
string username = 1; // 默认可选
int32 age = 2; // 默认可选,但无法区分 0 和未设置
}
在 Go 代码中:
user := &pb.User{}
fmt.Println(user.Age) // 输出 0,但无法确定是未设置还是真的设置为 0
使用 optional 关键字后(需要 proto3.15+):
syntax = "proto3";
message User {
string username = 1;
optional int32 age = 2; // 显式声明为可选
optional string phone = 3;
}
在 Go 代码中,optional 字段会生成为指针类型:
user := &pb.User{
Username: "alice",
Age: proto.Int32(25), // 使用指针
}
// 检查字段是否被设置
if user.Age != nil {
fmt.Printf("Age is set: %d\n", *user.Age)
} else {
fmt.Println("Age is not set")
}
proto2 vs proto3 对比
// proto2 风格
syntax = "proto2";
message CreateUserRequest {
required string username = 1; // 必填,未设置会导致序列化失败
optional string email = 2; // 可选,可以检测是否设置
optional string phone = 3; // 可选
repeated string tags = 4; // 数组类型
}
// proto3 风格(推荐)
syntax = "proto3";
message CreateUserRequest {
string username = 1; // 默认可选,但约定为必填(通过业务逻辑验证)
string email = 2; // 默认可选
optional string phone = 3; // 显式可选,可区分未设置和空字符串
repeated string tags = 4; // 数组类型
}
使用建议
- 推荐使用 proto3:proto3 是当前推荐的版本,语法更简洁
- 必填字段验证:在 proto3 中,通过业务逻辑代码验证必填字段,而不是使用
required - 使用 optional 的场景:
- 需要区分"未设置"和"零值"时(如年龄为 0 vs 未填写年龄)
- 需要区分"空字符串"和"未设置"时
- 部分更新场景(PATCH 操作)
实际示例:
syntax = "proto3";
// 用户信息更新请求
message UpdateUserRequest {
int64 user_id = 1; // 必填(业务逻辑验证)
optional string username = 2; // 可选更新
optional string email = 3; // 可选更新
optional string phone = 4; // 可选更新
optional int32 age = 5; // 可选更新,可以区分未设置和设置为 0
}
在 Go 代码中处理:
func (c *UserController) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateUserResponse, error) {
// 验证必填字段
if req.UserId == 0 {
return nil, status.Errorf(codes.InvalidArgument, "user_id is required")
}
// 只更新设置了的字段
updates := make(map[string]interface{})
if req.Username != nil {
updates["username"] = *req.Username
}
if req.Email != nil {
updates["email"] = *req.Email
}
if req.Age != nil {
updates["age"] = *req.Age
}
// 执行更新
err := c.UserService.UpdateUser(ctx, req.UserId, updates)
// ...
}
任意类型(google.protobuf.Any)
当你需要在 proto 消息中存储任意类型的数据时,可以使用 google.protobuf.Any 类型。它类似于 Go 中的 interface{}。
基本用法
syntax = "proto3";
// 导入 Any 类型
import "google/protobuf/any.proto";
// 定义具体的消息类型
message UserProfile {
string bio = 1;
string avatar_url = 2;
}
message CompanyProfile {
string company_name = 1;
string industry = 2;
}
// 使用 Any 类型存储不同类型的数据
message Account {
int64 id = 1;
string username = 2;
google.protobuf.Any profile = 3; // 可以存储 UserProfile 或 CompanyProfile
}
Go 代码中使用 Any
import (
"google.golang.org/protobuf/types/known/anypb"
pb "your-module/pb"
)
// 创建并打包 Any 消息
func CreateAccount() (*pb.Account, error) {
// 创建用户档案
userProfile := &pb.UserProfile{
Bio: "Software Engineer",
AvatarUrl: "https://example.com/avatar.jpg",
}
// 将 userProfile 打包为 Any 类型
profileAny, err := anypb.New(userProfile)
if err != nil {
return nil, err
}
// 创建账户
account := &pb.Account{
Id: 1,
Username: "alice",
Profile: profileAny,
}
return account, nil
}
// 解包 Any 消息
func ProcessAccount(account *pb.Account) error {
// 检查 Any 类型中存储的是什么类型
if account.Profile.MessageIs(&pb.UserProfile{}) {
// 解包为 UserProfile
var userProfile pb.UserProfile
if err := account.Profile.UnmarshalTo(&userProfile); err != nil {
return err
}
fmt.Printf("User bio: %s\n", userProfile.Bio)
} else if account.Profile.MessageIs(&pb.CompanyProfile{}) {
// 解包为 CompanyProfile
var companyProfile pb.CompanyProfile
if err := account.Profile.UnmarshalTo(&companyProfile); err != nil {
return err
}
fmt.Printf("Company: %s\n", companyProfile.CompanyName)
}
return nil
}
使用场景
- 动态类型数据:需要存储多种类型的数据,但类型在编译时不确定
- 插件系统:允许扩展系统接受未知类型的数据
- 通用 API:设计通用的 API 接口,支持多种请求/响应类型
- 事件系统:事件载荷可以是任意类型
注意事项
Any类型会增加序列化后的数据大小(需要存储类型信息)- 类型检查在运行时进行,不如直接使用具体类型安全
- 优先考虑使用
oneof(见下文),如果类型是已知的有限集合