前言
在现代软件开发中,系统之间的高效通信至关重要,尤其是在微服务架构和分布式系统中。为了高效地传输数据并保证跨语言的兼容性,Protocol Buffers
(简称 Protobuf
) 应运而生。Protobuf
是 Google
开发的一种轻量、高效的序列化数据格式。它被广泛应用于微服务、RPC
框架以及大数据处理等领域。
与传统的 JSON
或 XML
格式相比,Protobuf
的优势在于其更小的体积和更快的速度。它通过定义消息结构(Schema
)来进行数据的序列化和反序列化,支持多种编程语言,并且能够为开发人员提供一个明确且易于管理的数据传输模型。
本文将深入探讨如何在 Go
语言中使用 Protocol Buffers
(Protobuf
),全面覆盖从环境配置到实际应用的各个方面。我将逐步讲解如何安装和配置 Protobuf
编译器,编写和编译 .proto
文件,理解 Protobuf
的核心概念,如何定义和生成消息类型与服务接口。接着学习如何将其与 Go
结合,实现高效的序列化与反序列化操作。最后,文章还将介绍 Protobuf
的风格指南与最佳实践,帮助开发者在实际项目中更加规范、高效地使用 Protobuf
。
准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。
Protobuf 环境配置
安装 protobuf 编译器
Windows
1、下载 Protobuf
- 访问 Protobuf GitHub Releases 页面。
- 选择最新版本,并下载适用于
Windows
的protoc-<version>-win64.zip
或protoc-<version>-win32.zip
文件。
2、解压
- 解压下载的
ZIP
文件到你希望存放protoc
的目录。
3、添加环境变量
- 将
protoc
所在的目录添加到系统的环境变量中。这样你就可以从命令行中的任何位置运行它。 - 在“系统属性”中找到“环境变量”,然后在“Path”变量中添加
<protoc path>\bin
的路径。
4、验证安装
- 打开命令行,输入
protoc --version
,以检查是否安装成功。
$ protoc --version
libprotoc 29.3
MacOs
在 MacOs
系统上,你可以使用 Homebrew
安装 protoc
:
brew install protobuf
验证是否安装成功
$ protoc --version
libprotoc 29.3
Linux (Ubuntu/Debian)
在基于 Debian
的系统(如 Ubuntu
)上,你可以使用 apt
安装 protoc
:
sudo apt install protobuf-compiler
验证是否安装成功
$ protoc --version
libprotoc 3.6.1
使用 apt
安装 protoc
时,会默认安装一个较为稳定的版本,该版本可能不是最新版本。因此,如果想要安装最新版本,建议使用其他的方式下载最新版本的发布包,然后进行安装。例如:
# 下载发布包
$ wget https://github.com/protocolbuffers/protobuf/releases/download/v25.1/protoc-25.1-linux-x86_64.zip
# 解压到 /usr/local/bin 目录下
$ unzip protoc-25.1-linux-x86_64.zip -d /usr/local/bin/protoc-25.1-linux-x86_64
# 配置环境变量
$ vim ~/.bashrc
# 添加以下内容
export PATH=$PATH:/usr/local/bin/protoc-25.1-linux-x86_64/bin
# 激活配置文件
$ source ~/.bashrc
# 验证是否安装成功
$ protoc --version
libprotoc 25.1
安装 protoc-gen-go
protoc-gen-go
是 protoc
的一个插件,用于生成 Go
语言的代码。
通过下面的命令进行安装:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
验证是否安装成功:
$ protoc-gen-go --version
protoc-gen-go v1.31.0
初体验
首先在项目里面新建一个 proto
文件,假设文件名为 user.proto
,然后定义消息类型
syntax = "proto3";
package tutorial;
option go_package = "github.com/chenmingyong0423/blog/tutorial-code/go/protobuf/proto/user";
message User {
string name = 1;
int32 age = 2;
string email = 3;
}
然后执行以下命令,生成对应的 go
文件:
protoc --go_out=. --go_opt=paths=source_relative *.proto
这时我们就可以看到当前目录下多出了一个 user.pb.go
文件,该文件为 proto
代码编译后的 go
文件。
Protoc 命令常用参数
若要根据 proto
代码生成对应语言的代码(比如 Go
),我们需要使用 protoc
命令,这个命令在之前已经给出安装教程。protoc
命令的常用参数如下所示:
-I
或--proto_path
:指定import
的文件查找路径,可以指定多个路径,例如-Isrc -Iinclude
。这样编译器会在这几个路径下查找import
的.proto
文件。--<language>_out
:指定生成所指定的语言代码的输出目录,对于Go
:go_out=/directory
。--<language>_opt
:传递给指定语言插件的附加选项。作为protoc
的插件,它们有着特定的参数选项,如果我们想指定某个参数选项,需要通过<language>_opt
参数进行传递。例如:go_opt=paths=source_relative
,传递paths
参数选项给protoc-gen-go
插件。
在大多数情况下,通过指定 <language>_out
和 <language>_opt
参数,我们就可以满足代码生成的需求。值得一提的是,这些参数不限于单次使用;如果我们需要同时为多种语言生成代码,可以通过并行使用多个 <language>_opt
和 <language>_opt
来实现这一目标。
若想了解更多的参数,可以运行 protoc --help
命令进行查看。
protoc-gen-go 插件参数
protoc-gen-go
是一个用于生成 Go
代码的插件,该插件有两个重要参数:
paths
:控制go
文件生成的路径- 当
paths=import
时,输出文件将放置在 以Go
包的导入路径命名 的目录中(导入路径 由.proto
文件中的go_package
选项提供)。例如,Go
导入路径为github.com/chenmingyong0423/blog/tutorial-code/go/protobuf/proto/user
,那么输出的.go
文件将放置在github.com/chenmingyong0423/blog/tutorial-code/go/protobuf/proto/user/user.pb.go
。如果未指定paths
参数,paths
的值将默认为import
。 - 当
paths=source_relative
时,输出的.go
文件将与.proto
文件位于同一相对目录中。例如,.proto
文件位于proto/user/user.proto
,那么.go
文将在proto/user/user.pb.go
中生成。 module
:如果指定了module
参数,例如module=examples
,则生成的.go
文件将位于Go 包的导入路径
加上指定的模块目录下。例如,假设Go 包的导入路径
为protobuf
,并指定module=examples
,那么.go
文件将生成在protobuf/examples
目录中,例如:protobuf/examples/user.proto.go
。
protoc-gen-go
插件的参数需要通过 protoc
命令的 go_opt
参数进行传递,例如 go_opt=paths=source_relative
。
Protobuf 语法
定义消息类型
syntax = "proto3";
option go_package = "github.com/chenmingyong0423/blog/tutorial-code/protobuf/examples";
message User {
string name = 1;
int32 age = 2;
string email = 3;
}
通过 message
关键字定义一个消息类型。
字段定义
消息的字段定义格式为:[关键字] 类型 字段名 = 编号;
,例如 string name = 1;
、optional string name = 1;
。
要点
- 必须为消息定义中的每个字段指定一个
1
到536,870,911
之间的数字,并遵守以下限制:- 给定的编号在该消息的所有字段中必须是唯一的。
- 字段编号
19,000
到19,999
被保留给Protocol Buffers
实现。如果你在消息中使用了这些保留的字段编号,协议缓冲区编译器会报错。 - 不能使用任何之前已经保留的字段编号,也不能使用已经分配给扩展的字段编号。
- 在
proto3
中,字段默认被标记为optional
,这意味着你可以不为某个字段赋值,它会使用该字段类型的默认值,同时也可以区分该字段是否被 赋值,即使该字段的值为默认值。
字段类型
标量类型(Scalar Types)
这些类型表示常见的数据类型,如整数、浮点数、布尔值、字符串等。
类型 | 默认值 | 备注 |
---|---|---|
double | 0.0 | |
float | 0.0 | |
int32 | 0 | 32 位有符号整数,使用 变长编码(Variable-length encoding)。对于负数的编码效率较低。 如果字段值经常是负数,建议使用 sint32 ,因为 sint32 更有效地编码负数。 |
int64 | 0 | 64 位有符号整数,使用 变长编码(Variable-length encoding)。对于负数的编码效率较低。 如果字段值经常是负数,建议使用 sint64 ,因为 sint64 更有效地编码负数。 |
uint32 | 0 | 32 位无符号整数,使用 变长编码(Variable-length encoding)。 |
uint64 | 0 | 64 位无符号整数,使用 变长编码(Variable-length encoding)。 |
sint32 | 0 | 32 位有符号整数,使用 变长编码(Variable-length encoding)。与 int32 类似,但优化了负数的编码方式。 |
sint64 | 0 | 64 位有符号整数,使用 变长编码(Variable-length encoding)。与 int64 类似,但优化了负数的编码方式。 |
fixed32 | 0 | 始终使用 4 个字节进行编码。比 uint32 更有效,如果值大于 228)。 |
fixed64 | 0 | 始终使用 8 个字节进行编码。比 uint64 更有效,如果值大于 256)。 |
sfixed32 | 0 | 始终使用 4 个字节进行编码的有符号整数。 |
sfixed64 | 0 | 始终使用 8 个字节进行编码的有符号整数。 |
bool | false | 布尔类型,只有两个值 true 或 false 。 |
string | 空字符串 | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不能超过 232。 |
bytes | 空字节 | 可以包含不超过 232 的任意任意字节序列。 |
枚举类型(Enums)
枚举类型允许定义一组命名常量,通常用于表示状态、选项、类别等。
enum Status {
PENDING = 0;
IN_PROGRESS = 1;
COMPLETED = 2;
}
- 枚举值必须是 整数类型。
- 默认情况下,枚举值的第一个常量为 0,表示默认值。
消息类型(Message Types)
message
是 Protobuf
中的复合类型,用来表示一组相关的数据字段。每个字段可以是不同的类型,包括标量类型、枚举类型、其他消息类型等。
message User {
string name = 1;
int32 age = 2;
string email = 3;
}
- 嵌套消息:你还可以在一个消息中定义其他消息类型。
message AddressBook {
message User {
string name = 1;
string email = 2;
}
repeated User user= 1; // 这个字段是一个列表,包含多个 User
}
特殊类型
除了基本的标量、枚举以及消息类型,ProtoBuf
还提供了几种特殊的类型,用于处理更复杂的需求。
repeated
:表示字段可以有多个值,相当于一个数组或列表。
message User {
repeated string phones = 1; // 可以包含多个字符串
}
map
:表示键值对集合,相当于字典或哈希表。键可以是标量类型(浮点类型和bytes
除外),值可以是除另一个map
之外的任何类型。。
message User {
map <string, int32> scores = 1;
}
使用 map
类型的一些注意事项如下:
map
字段不能使用repeated
关键字。- 为
.proto
生成文本格式时,映射按键排序。数字键按数字排序。 map
的键值对在wire
格式中的顺序以及在迭代时的顺序是未定义的,因此你不能依赖map
中元素的顺序。- 在生成
.proto
的文本格式时,map
会按键进行排序。对于数值型的键,排序会按数字顺序进行。 - 在解析
map
或进行合并时,如果出现重复的键,最后一个键值会被使用。在从文本格式解析时,如果遇到重复的键,解析可能会失败。 - 如果你为
map
字段提供了一个键但没有提供值,则序列化时的行为取决于语言:- 在
C++
、Java
、Kotlin
和Python
中,序列化时会使用该类型的默认值。 - 在其他语言中,如果没有提供值,则该字段不会被序列化。
- 在
- 在同一作用域中,不能存在一个名为
map foo
的字段和一个名为FooEntry
的符号,因为FooEntry
已经被用于 map 的实现。 Any
:表示任意类型,它可以让字段存储不同类型的数据,而不需要在消息定义时提前知道这些类型。要使用Any
类型,您需要导入google/protobuf/any.proto
。
import "google/protobuf/any.proto";
message User {
google.protobuf.Any data = 1;
}
oneof
:一种特殊的字段类型,允许在一个消息中 定义多个字段,但在任何时候只能 设置其中一个字段。你可以添加任何类型的字段,map
字段和repeated
字段除外。如果需要向oneof
添加重复字段,可以使用包含重复字段的消息类型。
message MyMessage {
oneof message_data {
string text = 1;
int32 number = 2;
User user = 3;
}
}
使用 oneof
类型的一些注意事项如下:
- 为
oneof
字段赋值时,它会自动清除同一oneof
中的其他字段的值。 - 如果解析时遇到同一个
oneof
中的多个字段,则只有最后一个字段会在解析的消息中保留其值。- 首先检查同一个
oneof
中的其他字段是否已经设置。如果有其他字段已设置,则清除它。 - 然后按正常方式解析该字段,就好像它不属于
oneof
一样:- 基本类型 会覆盖已经设置的值。
- 消息类型 会与已设置的值合并。
- 首先检查同一个
oneof
字段不能使用repeated
关键字。- 反射
API
对oneof
字段有效 你可以通过反射API
来访问和修改oneof
字段的值。 - 如果你为
oneof
字段设置默认值(例如将int32
类型的字段设置为 0),即使该字段的值是默认值,oneof
的 “case” 也会被设置,并且该值会被序列化到wire
格式中。
定义服务
如果需要在 RPC
(远程过程调用)系统中使用你的消息类型,可以在 .proto
文件中定义一个 RPC
服务接口,协议缓冲编译器会为你生成服务接口代码和存根代码,适用于你选择的编程语言。例如,如果你想定义一个 RPC
服务,包含一个方法,该方法接受 SearchRequest
并返回 SearchResponse
,你可以在 .proto
文件中这样定义:
service SearchService {
rpc Search(SearchRequest) returns (SearchResponse);
}
Go Protobuf 基础
掌握了 protobuf
基本的语法之后,接下来我们要了解 proto
代码与 go
代码之间的关系。下面将围绕着以下示例代码逐步进行讲解。
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
option go_package = "github.com/chenmingyong0423/blog/tutorial-code/go/protobuf/protos/user";
message User {
int32 id = 1;
string name = 2;
int32 age = 3;
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp birth = 5;
}
enum PhoneType {
// 个人手机
PHONE_TYPE_MOBILE = 0;
// 工作电话
PHONE_TYPE_WORK = 1;
}
包声明
syntax = "proto3";
package tutorial;
option go_package = "github.com/chenmingyong0423/blog/tutorial-code/go/protobuf/protos/user";
.proto
文件以 package
声明开头,这有助于避免不同项目之间的命名冲突。然而,这里的 package
并不对应 Go
语言中的 package
。协议缓冲编译器(protoc
)会根据 .proto
文件中 go_package
字段的导入路径来确定 Go
代码中的包名,通常是该路径的最后一个部分。例如,基于示例代码生成的 Go
代码包名将是 user
。
包导入
如果在 .proto
文件中引入了标准库或第三方库,编译生成的 Go
代码中也会反映这一点。例如,若引入 google/protobuf/timestamp.proto
,在 Go
代码中对应的导入路径为:
timestamppb "google.golang.org/protobuf/types/known/timestamppb"
结构体
message User {
int32 id = 1;
string name = 2;
int32 age = 3;
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp birth = 5;
}
enum PhoneType {
// 个人手机
PHONE_TYPE_MOBILE = 0;
// 工作电话
PHONE_TYPE_WORK = 1;
}
协议缓冲编译器(protoc
)会将 protobuf
中的类型转换为 Go
语言中对应的类型。例如,message
类型会转换为 Go
中的 struct
结构体,而由于 Go
没有内建的枚举类型,enum
类型会被转换为 Go
的自定义类型。所生成的部分代码如下所示:
type User struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Age int32 `protobuf:"varint,3,opt,name=age,proto3" json:"age,omitempty"`
Phones []*User_PhoneNumber `protobuf:"bytes,4,rep,name=phones,proto3" json:"phones,omitempty"`
Birth *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=birth,proto3" json:"birth,omitempty"`
}
type PhoneType int32
const (
// 个人手机
PhoneType_PHONE_TYPE_MOBILE PhoneType = 0
// 工作电话
PhoneType_PHONE_TYPE_WORK PhoneType = 1
)
type User_PhoneNumber struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Number string `protobuf:"bytes,1,opt,name=number,proto3" json:"number,omitempty"`
Type PhoneType `protobuf:"varint,2,opt,name=type,proto3,enum=PhoneType" json:"type,omitempty"`
}
Protobuf 类型与 Go 类型之间的映射关系
Protobuf
类型与 Go
类型之间有着明确的映射关系,理解这些映射关系对于正确使用 Protobuf
在 Go
中非常重要。以下是一些常见的映射规则:
Protobuf 类型 | Go 类型 |
---|---|
double | float64 |
float | float32 |
int32 | int32 |
int64 | int64 |
uint32 | uint32 |
uint64 | uint64 |
sint32 | int32 |
sint64 | int64 |
fixed32 | uint32 |
fixed64 | uint64 |
sfixed32 | int32 |
sfixed64 | int64 |
bool | bool |
string | string |
bytes | []byte |
message | struct |
enum | 自定义类型(通常是 int32) |
repeated | slice |
map | map |
读写消息示例
首先,我们需要创建一个名为 protobuf
的目录,并进入该目录初始化一个 Go
项目。接下来,在 proto/user
目录中创建一个名为 user.proto
的文件,文件内容使用之前提供的示例代码。项目目录结构如下所示:
.
├── go.mod
├── go.sum
└── proto
└── user
└── user.proto
然后在 proto
目录下,通过以下命令使用 protoc
编译 .proto
文件,生成对应的 Go
代码:
protoc --go_out=. --go_opt=paths=source_relative *.proto
接下来将基于生成的 Go
代码演示如何进行 Protobuf
消息的写入(序列化) 和 读取(反序列化) 操作。
在此之前,我们需要安装 proto
模块:
go get google.golang.org/protobuf/proto
序列化消息(写入)
// 写入消息
user := pb.User{
Id: 1,
Name: "陈明勇",
Age: 18,
Phones: []*pb.User_PhoneNumber{
{
Number: "18888888888",
Type: pb.PhoneType_PHONE_TYPE_MOBILE,
},
{
Number: "12345678901",
Type: pb.PhoneType_PHONE_TYPE_WORK,
},
},
Birth: timestamppb.New(time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC)),
}
out, err := proto.Marshal(&user)
if err != nil {
panic(err)
}
err = os.WriteFile("user.bin", out, 0644)
if err != nil {
panic(err)
}
反序列化消息(读取)
// 读取消息
in, err := os.ReadFile("user.bin")
if err != nil {
panic(err)
}
user2 := &pb.User{}
err = proto.Unmarshal(in, user2)
if err != nil {
panic(err)
}
// id:1 name:"陈明勇" age:18 phones:{number:"18888888888"} phones:{number:"12345678901" type:PHONE_TYPE_WORK} birth:{seconds:915148800}
fmt.Println(user2)
完整示例:序列化与反序列化
新建 main.go
文件并写入以下内容:
package main
import (
"fmt"
pb "github.com/chenmingyong0423/blog/tutorial-code/go/protobuf/proto/user"
"os"
"time"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
func main() {
// 写入消息
user := pb.User{
Id: 1,
Name: "陈明勇",
Age: 18,
Phones: []*pb.User_PhoneNumber{
{
Number: "18888888888",
Type: pb.PhoneType_PHONE_TYPE_MOBILE,
},
{
Number: "12345678901",
Type: pb.PhoneType_PHONE_TYPE_WORK,
},
},
Birth: timestamppb.New(time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC)),
}
out, err := proto.Marshal(&user)
if err != nil {
panic(err)
}
err = os.WriteFile("user.bin", out, 0644)
if err != nil {
panic(err)
}
// 读取消息
in, err := os.ReadFile("user.bin")
if err != nil {
panic(err)
}
user2 := &pb.User{}
err = proto.Unmarshal(in, user2)
if err != nil {
panic(err)
}
// id:1 name:"陈明勇" age:18 phones:{number:"18888888888"} phones:{number:"12345678901" type:PHONE_TYPE_WORK} birth:{seconds:915148800}
fmt.Println(user2)
}
通过 proto.Marshal
和 proto.Unmarshal
函数,我们可以对 Protobuf
消息进行序列化(写入)和反序列化(读取)操作。
使用 go run main.go
命令,程序即可成功运行。
Protobuf 风格指南
为了确保 .proto
文件中协议缓冲消息定义及其对应类的结构一致且易于阅读。我们需要遵循这些规范。
需要注意的是,协议缓冲的风格在不断演进,因此我们可能会遇到采用不同风格或规范编写的
.proto
文件。在修改这些文件时,需要尽量遵循已有的风格,保持一致性是非常重要的。当然,在创建新的.proto
文件时,建议采用当前最新的的最佳实践和风格。
标准文件格式
- 保持每行字符长度不超过 80 个字符。
- 使用 2 个空格作为缩进。
- 字符串应优先使用双引号。
文件结构
文件名应采用小写蛇形命名法(lower_snake_case.proto
)。
所有文件应按以下顺序组织:
- 许可头(如果适用)
- 文件概述
- 语法版本
- 包声明
- 导入包(按字母顺序排序)
- 文件选项
- 其他内容
包声明
- 包名应采用小写字母。
- 包名应具有唯一性,通常基于项目名称,并且可以根据包含协议缓冲类型定义的文件路径进行命名。例如文件路径为
proto/user/user.proto
,则包名可以是proto.user
消息和字段命名
对于消息名称,使用 PascalCase
(首字母大写)命名风格,例如 SongServerRequest
。对于缩写,推荐将其为一个整体,保持首字母大写,而不是拆分字母:例如 GetDnsRequest
,而不是 GetDNSRequest
。Dns
作为一个整体,首字母大写。
对于字段名称(包括 oneof
字段和扩展名),使用 lower_snake_case
(小写字母,单词间用下划线分隔):例如 song_name
。
Repeated 字段
对 Repeated
字段使用复数名称。例如 repeated string keys
。
Enum 字段
- 枚举类型的命名:
- 使用 PascalCase(首字母大写)来命名枚举类型。例如:
FooBar
。
- 使用 PascalCase(首字母大写)来命名枚举类型。例如:
- 枚举值的命名:
- 使用 CAPITALS_WITH_UNDERSCORES(大写字母,并用下划线分隔)来命名枚举值。例如:
FOO_BAR_UNSPECIFIED
,FOO_BAR_FIRST_VALUE
。
- 使用 CAPITALS_WITH_UNDERSCORES(大写字母,并用下划线分隔)来命名枚举值。例如:
- 每个枚举值后应以分号结尾,而不是逗号。
- 避免命名冲突:建议为每个枚举值加上枚举名称前缀或将枚举嵌套在消息内部。
- 使用顶级枚举:如果可以,避免嵌套枚举。
- 零值枚举:枚举的零值命名应为
UNSPECIFIED
。
服务(Service)
如果你的 .proto
文件中定义了 RPC
服务,应该对 服务名称 和 RPC 方法名称 都使用 PascalCase(首字母大写)命名规则:
service FooService {
rpc GetSomething(GetSomethingRequest) returns (GetSomethingResponse);
rpc ListSomething(ListSomethingRequest) returns (ListSomethingResponse);
}
Protobuf 最佳实践
- 不要重用标签号
不要重用标签号。重用标签号会导致反序列化错误。即使你认为没有人在使用该字段,也不要重用标签号,因为历史中可能已经有已序列化的
proto
数据,或者其他服务的旧代码可能会受到影响。 - 删除字段后保留标签号
当你删除一个字段时,应该保留其标签号,以避免未来有人不小心重用该标签号。仅保留
2
和3
等数字即可。你还可以保留已删除字段的名称,避免它们被重用:例如,reserved "foo", "bar";
。 - 删除枚举值时保留标签号
同样,删除不再使用的枚举值时,应该保留它们的标签号,以免他人误用。可以像字段一样保留
2
和3
等标签号,并保留已删除的枚举值名称:例如,reserved "FOO", "BAR";
。 - 避免改变字段类型
除非是深思熟虑,否则不要改变字段的类型。这会导致反序列化失败。虽然有些类型的转换(如
int32
转uint32
)是安全的,但改变消息类型会破坏兼容性,除非新类型是旧类型的超集。 - 不要添加必填字段
永远不要添加必填字段,而应该通过文档注释来指定
API
合同的要求。proto3
移除了必填字段的支持,所有字段应当是可选的或重复的。这样可以避免未来需求变化时强制使用不再逻辑上需要的字段。 - 不要创建包含大量字段的消息
尽量避免在同一消息中定义大量字段(例如:几百个字段)。过大的
proto
文件会增加内存使用,甚至可能导致生成的代码无法编译。建议将大型消息拆分为多个小的消息。 - 为枚举添加一个未指定值
枚举应该包含一个默认的
FOO_UNSPECIFIED
值,作为枚举声明的第一个值。这样在添加新值时,旧客户端会将字段视为未设置,并返回默认值(即枚举的第一个值)。此外,枚举值应使用tag 0
作为UNSPECIFIED
的默认值。 - 使用通用类型和常用类型
推荐使用一些已定义的通用类型(如
duration
、timestamp
、date
、money
等),而不是自己定义类似的类型。这样可以减少重复定义,同时也能确保跨语言的一致性。 - 在单独的文件中定义消息类型undefined 每个
proto
文件最好只定义一个消息、枚举、扩展、服务或循环依赖。将相关类型放在一个文件中会更容易进行重构和维护,也能确保文件不被过度膨胀。 - 不要更改字段的默认值
永远不要更改字段的默认值,这样会导致客户端和服务端的版本不兼容。
proto3
移除了为字段设置默认值的能力,因此,最好避免更改字段的默认值。 - 避免将
repeated
类型转换为标量类型 不要将repeated
字段改为标量类型,这样会丢失数据。对于proto3
的数值类型字段,转换将会丢失字段数据。 - 避免使用文本格式消息进行交换
文本格式(如
JSON
和文本格式)的序列化方法并不适合用于数据交换。它们将字段和枚举值表示为字符串,因此在字段或枚举值重命名或新增字段时,旧代码会导致反序列化失败。应尽可能使用二进制格式进行数据交换,文本格式仅限于调试和人工编辑。 - 永远不要依赖于跨构建的序列化稳定性
Protobuf
的序列化稳定性无法保证跨不同的二进制文件或同一二进制文件的不同构建版本。不要依赖序列化稳定性来构建缓存键等。 - 避免使用语言关键字作为字段名称
避免使用在目标语言中作为关键字的字段名称,因为这可能导致
protobuf
自动更改字段名称或提供特殊访问方式。还应避免在文件路径中使用关键字。
小结
本文介绍了如何在 Go
中使用 Protobuf
,涵盖了环境配置、语法、集成步骤、风格指南和最佳实践等内容。通过本文,你可以快速上手 Go
与 Protocol Buffers
的集成,掌握消息类型的定义、代码的生成以及消息的序列化与反序列化流程。