Golang 28_Golang与Protobuf

一、Protobuf 简介

1.1 Protobuf 是什么

Protocol Buffers (简称 Protobuf )是 Google 公司开源的一种轻便高效的结构化数据存储格式,用于序列化和反序列化结构化数据的代码生成器。它可以用于通讯协议和数据存储等领域。

Protobuf 是以 .proto 文件形式定义结构化数据的方式和格式。并且通过代码生成器生成各平台(C/C++、Java、Python、Go 等)的数据访问类,这些生成的类可以用来在对应的语言中解析、序列化 Protobuf 数据。

1.2 Protobuf 的优点

Protobuf 作为一种数据格式和工具,有以下优点:

1、性能高,序列化和反序列化速度很快

Protobuf 采用二进制格式存储数据,相比 XML 和 JSON 等格式,可以大幅减少数据体积, serialization 和 deserialization 的性能也更优。这对于高性能场景非常有利。

2、跨平台,多语言支持广泛

Protobuf 提供了标准的 .proto 文件格式和数据描述语法,然后可以通过 protoc 工具,自动生成各主流语言的数据访问类,如 C/C++、Java、Python、Go 等。

这保证了在不同平台和不同语言 scenarios 下,可以解析和验证一致的 Protobuf 数据。

3、定义结构化数据格式,方便维护升级

通过 .proto 文件定义数据格式,可以清晰界定不同版本数据格式的兼容关系,格式修改后也方便使用旧格式数据。

4、数据体积小,便于存储和传输

相比 XML、JSON,Protobuf 的二进制编码可以大幅减小数据体积,节省存储和网络传输成本。

5、扩展性好,灵活支持新增字段

通过定义可选和 Required 字段,可以轻松添加和删除消息中的字段,而不影响已有字段的序号,便于数据格式的扩展和演进。

二、Go 语言中使用 Protobuf

2.1 在 Go 语言中安装 Protobuf 库

在 Go 语言中使用 Protobuf 主要依赖 google 开源的 golang/protobuf 库,使用以下命令安装:

1
go get -u github.com/golang/protobuf/proto

安装完成后,在代码中 import 此库:

1
import "github.com/golang/protobuf/proto"

2.2 使用 protoc 编译.proto 文件

编写 .proto 文件后,需要使用 protoc 命令生成 Go 代码,例如:

1
protoc --go_out=. message.proto

这会根据 message.proto 中的消息定义,生成 Go 语言版本的访问类,存放在 message.pb.go 文件中。

2.3 Protobuf 消息的编码和解码

golang/protobuf 库中主要包含下面两个函数,用来序列化和反序列化 Protobuf 消息:

1
2
func Marshal(pb Message) ([]byte, error)
func Unmarshal(buf []byte, pb Message) error

其中 Message 是一个满足 protobuf.Message 接口的 Protobuf 消息对象,可以是通过 .proto 生成的 pb.go 文件中定义的类型,也可以是动态消息。

2.4 Protobuf 服务的定义

除了用于数据存储、网络通信外,Protobuf 也可以用来定义服务接口(RPC 服务)。语法如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

message SearchRequest {
  string query = 1; 
  int32 page_number = 2;  
  int32 result_per_page = 3;
}
  
message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;  
  string title = 2;  
  repeated string snippets = 3;
}

这样就定义了一个 RPC 服务 interface,包含一个 Search 方法。然后客户端和服务器端通过实现这个 interface,来发送、处理服务请求和响应。

服务端需要实现服务接口定义的方法,客户端需要调用这个接口方法,传递请求参数,获取响应结果。

这两个函数可以方便的在任意 Go 类型与 Protobuf 二进制格式之间进行转换。

三、Protobuf 消息的定义

通过 .proto 文件, 可定义 Protobuf 中的消息结构。Protobuf 消息由一系列字段组成,使用 message 定义,每个消息可包含多种类型的字段。

3.1 3.1 消息类型

Protobuf 支持标量类型、复合类型的消息定义。

  • 标量类型: 包括整型、浮点型、布尔型、字符串等;
  • 复合类型: 主要是其它消息类型,一个消息字段可以引用其它消息类型。

3.2 标量类型

语法格式如下:

1
[修饰符] 类型名 字段名 = 字段号;

常用标量类型和修饰符总结如下:

  • int32,int64 - 有符号整型
  • uint32,uint64 - 无符号整型
  • bool - 布尔类型
  • string - 字符串类型
  • bytes - 字节数组
  • float,double - 浮点类型
  • repeated - 重复类型,表示数组
  • required - 必填字段
  • optional - 可选字段,默认值

示例:

1
2
3
4
5
message Person {
  required string name = 1;
  required int32 age = 2;  
  optional string email = 3;
}

这定义了一个 Person 消息,包含必填的 nameage 字段和可选的 email 字段。

使用 protobuf 可定义一对请求和响应消息,用于 RPC 服务接口的输入和输出参数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/ SearchRequest请求消息
message SearchRequest {
  required string query = 1;
  optional int32 page = 2;  
  ...
}

// SearchResponse响应消息  
message SearchResponse {
  repeated Result results = 1;
  optional int32 total_results = 2;
}

这样通过一对请求响应消息消息,定义了服务接口的入参和返回值格式。

3.3 import 公共 proto 文件

为了重用消息定义和其它 .proto 文件的内容,可以用 import 语句导入其它 .proto 文件。

例如:

1
import "other/other.proto";

这样就可以直接引用 other.proto 中定义的消息、枚举等。

3.4 使用 options 设置项

Protobuf 支持自定义 options 字段,对消息、枚举进行注解或设置生成参数:

1
2
3
message Foo {
  optional string text = 1 [(custom_option) = "hello world"]; 
}

这为 text 字段添加一个自定义 option 注解。

四、Go 语言 Protobuf 实践

下面以一个完整的例子,演示下 Go 语言中使用 Protobuf 的整个流程。

4.1 定义 Protobuf 消息

编写一个 person.proto 文件,定义 Person 消息格式:

1
2
3
4
5
6
7
8
9
syntax = "proto3";

package tutorial;

message Person {
  string name = 1;
  int32 age = 2; 
  string email = 3;
}

4.2 生成 Go 代码

然后使用 protoc 命令根据 person.proto 生成 Go 语言代码:

1
protoc --go_out=. person.proto

这会生成一个 person.pb.go 文件。

4.3 序列化和反序列化 Protobuf 消息

有了生成的 Go 访问类, 就可很方便的在 Go 代码中处理 Person 消息。

例如序列化和反序列化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
    "log"
    "github.com/golang/protobuf/proto"
    "path/to/personpb" // Import the generated personpb package
)

func main() {
    p := &personpb.Person{
        Name:  "John Doe",
        Age:   30,
        Email: "john@email.com",
    }

    data, err := proto.Marshal(p)
    if err != nil {
        log.Fatal("marshaling error: ", err)
    }

    // Handle the marshaled data, for example, print it
    log.Printf("Marshaled data: %v", data)

    // If you want to do something with the marshaled data, you can use it here
}

4.4 Protobuf 服务端和客户端

可利用 Protobuf 来定义服务接口,下面演示服务器和客户端的实现。

1
2
3
4
5
6
7
8
9
person.proto 中定义服务:

service PersonService {
  rpc GetPerson(GetPersonRequest) returns (Person) {}
}

message GetPersonRequest {
  string name = 1;
}

这定义了一个 PersonService,包含获取 Person 的 GetPerson 方法。

在服务器代码中实现这个接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type server struct{}

func (s *server) GetPerson(ctx context.Context, req *GetPersonRequest) (*Person, error) {
  // 从数据库中获取Person对象并返回
}

func main() {
  lis, err := net.Listen("tcp", ":50051")
  srv := grpc.NewServer()
  pb.RegisterPersonServiceServer(srv, &server{}) 
  srv.Serve(lis)
}

在客户端, 可调用这个接口:

1
2
3
4
conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := pb.NewPersonServiceClient(conn)

resp, err := client.GetPerson(context.Background(), &req)

这样通过 gRPC 框架,就可以访问服务器定义的 Protobuf 服务。

4.5 Protobuf 与 gRPC 集成

Protobuf 定义的消息和服务可以很容易的在 gRPC 框架中使用,gRPC 正是通过 Protobuf 接口定义实现服务通信的。

服务器端实现 Protobuf 接口,客户端调用接口,二者通过 gRPC 通讯。

五、Protobuf 使用注意事项和经验

5.1 版本控制

为了兼容旧版本,在修改消息定义时,应该谨慎地创建新字段而不是删除旧字段。

5.2 向后兼容

  • 对于 int32、uint32、int64、uint64、bool、string、bytes 字段,新代码可以读写旧消息,前向后兼容性是没有问题的。
  • 对于 repeated 字段,删除或者顺序改变字段号,会造成不兼容。
  • 新增 optional 或 repeated 字段,前向后兼容性是没有问题。但是新增 required 字段则会造成解析问题。

所以新增字段时,应使用 optional 而不是 required。

5.3 包含大数据量字段

由于 Protobuf 是二进制编码的,如果有字段包含非常大的数据(如图片、视频),会大幅增加消息大小。

这时可以考虑通过指针引用独立文件的形式,避免消息体积过大。

5.4 Protobuf 优缺点

相比 XML 和 JSON 数据格式, Protobuf 作为一种高效的结构化数据存储和交换格式,具有以下优点:

  • 编码效率高,序列化后数据体积小
  • 解析速度快
  • 支持数据格式升级与兼容
  • 支持定义服务接口
  • 跨平台跨语言,通过编译支持各平台访问

当然也存在一些限制,比如不适合处理频繁修改的数据格式,不支持数据查询等。所以 Protobuf 在很多性能敏感、跨平台的场景下,可以发挥很好的作用。

5.5 在 Go 语言项目中的作用

在 Go 语言中,Protobuf 可以用于:

  • 定义项目中的数据结构体
  • 网络服务的请求响应参数和结果
  • RPC 服务接口定义
  • 数据存储格式定义

通过 Protobuf 接口定义,可以实现服务端和客户端的松耦合。

并且利用 Protobuf 接口语言无关性,可以支持多语言访问后端 Go 服务,实现更好的语言融合。