Go语言与Protobuf
1. Protobuf 介绍
Protobuf 是 Protocol Buffers 的简称 ,是Google开源的的一种数据描述语言,其初始定位类似于 XML 和 json 等数据描述语言,Protobuf 最常见的使用场景是用于RPC系统中,这是因为protobuf 是一种轻便高效的结构化数据存储格式,可以序列化,比较适合做数据存储或者RPC数据交互格式
2. Protobuf 特点
优点 :
- protobuf 以高效二进制存储,占用空间比 XML 小 3~10倍,解析速度快 20 ~ 100倍
- 代码生成机制
- 多编程语言支持
- 兼容性比较好
缺点 :
- 可读性差,调试相对困难
想了解 protobuf 具体详情 参考如下 :
3. Protobuf 环境配置
3.1 安装protobuf 基础工具
-
根据自己开发系统下载protobuf 编译器
-
配置环境变量
-
验证是否安装成功
# 查看protobuf编译器版本 $ protoc --version libprotoc 3.11.0 # 查看把帮助文档 $ protoc --help . . . --cpp_out=OUT_DIR Generate C++ header and source. --csharp_out=OUT_DIR Generate C# source file. --java_out=OUT_DIR Generate Java source file. --js_out=OUT_DIR Generate JavaScript source. --objc_out=OUT_DIR Generate Objective C header and source. --php_out=OUT_DIR Generate PHP source file. --python_out=OUT_DIR Generate Python source file. --ruby_out=OUT_DIR Generate Ruby source file. @<filename> Read options and filenames from file. If a relative file path is specified, the file will be searched in the working directory. The --proto_path option will not affect how this argument file is searched. Content of the file will be expanded in the position of @<filename> as in the argument list. Note that shell expansion is not applied to the content of the file (i.e., you cannot use quotes, wildcards, escapes, commands, etc.). Each line corresponds to a single argument, even if it contains spaces.
3.2 安装编程语言插件
我们选择Go语言的代码生成插件
在命令行下执行
go get github.com/golang/protobuf/protoc-gen-go
该命令执行完成之后,我们会看到这么一个目录
$GOPATH/src/github.com/golang/protobuf
安装成功之后会在 $GOPATH/bin 目录下生成
protoc-gen-go
的可执行文件
安装好Go语言的代码生成插件之后,我们测试一下,步骤如下:
-
编写一个以
.proto
为后缀的protobuf文件|
|__example
|____ demo2.proto
文件是 example/demo2.proto
syntax = "proto3"; // 使用 protobuf3的语法 package example; message Person { string name = 1; int32 age = 2; enum Gender { MALE = 0; FEMALE = 1; UNKNOWN = 2; } message Other { string addr = 1; string hobby = 2; Gender g = 3; } Other info = 3; }
-
使用
protoc-gen-go
生成 go语言代码tips : 如果执行下面命令不生效,可以尝试将
protoc-gen-go
的可执行文件复制一份到$GOROOT/bin/
目录下protoc --go_out=. example/demo2.proto
对上述命令简单解释一下:
--go_out
参数是告诉protoc
编译器去加载对应的protoc-gen-go
工具,通过这个工具生成对应的Golang 代码=.
表示生成代码的存放在当前目录下,当然也可指定到其他文件目录下example/demo2.protoc
是protobuf文件的路径在实际使用过程中我们灵活使用参数即可
我们将看到一个文件名为
demo2.pb.go
文件生成|
|__example
|____ demo2.proto
|____ demo2.pb.go
这个新增的文件就是自动生成的 go 语言代码 ,我们看一下
demo2.pb.go
的部分代码- 开发过程中我们定义好
.proto
文件之后,这段代码是插件自动生成,不同的编程语言语法上会有区别 - 生成的代码通常不需要我们关注
- 业务层中只按需使用自动生成代码文件中提供好的
工具
// Code generated by protoc-gen-go. DO NOT EDIT. // source: demo2.proto package example import ( fmt "fmt" proto "github.com/golang/protobuf/proto" math "math" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package type Person_Gender int32 const ( Person_MALE Person_Gender = 0 Person_FEMALE Person_Gender = 1 Person_UNKNOWN Person_Gender = 2 ) var Person_Gender_name = map[int32]string{ 0: "MALE", 1: "FEMALE", 2: "UNKNOWN", } var Person_Gender_value = map[string]int32{ "MALE": 0, "FEMALE": 1, "UNKNOWN": 2, } func (x Person_Gender) String() string { return proto.EnumName(Person_Gender_name, int32(x)) } func (Person_Gender) EnumDescriptor() ([]byte, []int) { return fileDescriptor_80e54830e2bc2dba, []int{0, 0} } type Person struct { Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` Age int32 `protobuf:"varint,2,opt,name=age,proto3" json:"age,omitempty"` Info *Person_Other `protobuf:"bytes,3,opt,name=info,proto3" json:"info,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Person) Reset() { *m = Person{} } func (m *Person) String() string { return proto.CompactTextString(m) } func (*Person) ProtoMessage() {} func (*Person) Descriptor() ([]byte, []int) { return fileDescriptor_80e54830e2bc2dba, []int{0} } func (m *Person) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Person.Unmarshal(m, b) } func (m *Person) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Person.Marshal(b, m, deterministic) } func (m *Person) XXX_Merge(src proto.Message) { xxx_messageInfo_Person.Merge(m, src) } func (m *Person) XXX_Size() int { return xxx_messageInfo_Person.Size(m) } func (m *Person) XXX_DiscardUnknown() { xxx_messageInfo_Person.DiscardUnknown(m) } var xxx_messageInfo_Person proto.InternalMessageInfo func (m *Person) GetName() string { if m != nil { return m.Name } return "" } func (m *Person) GetAge() int32 { if m != nil { return m.Age } return 0 } func (m *Person) GetInfo() *Person_Other { if m != nil { return m.Info } return nil } type Person_Other struct { Addr string `protobuf:"bytes,1,opt,name=addr,proto3" json:"addr,omitempty"` Hobby string `protobuf:"bytes,2,opt,name=hobby,proto3" json:"hobby,omitempty"` G Person_Gender `protobuf:"varint,3,opt,name=g,proto3,enum=example.Person_Gender" json:"g,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` }
- 开发过程中我们定义好
3.3 Protobuf 基础使用
|
|__example
|____ demo2.proto
|____ demo2.pb.go
|__ main.go
main.go
package main
import (
"GoNote/chapter10/demo9/example"
"fmt"
"github.com/golang/protobuf/proto"
"log"
)
func main() {
// 相当于示例化一个Person的对象
// 这个Person的结构体就是我们通过代码自动生成的
d1 := example.Person{
Name: "tom",
Age: 99,
Info: &example.Person_Other{
Addr: "beijing",
Hobby: "code",
G: example.Person_MALE,
},
}
// 进行序列化操作
d1Encode, err := proto.Marshal(&d1)
if err != nil {
log.Fatal(err.Error())
}
fmt.Println(d1Encode)
// 进行反序列化操作
d1Decode := example.Person{}
err = proto.Unmarshal(d1Encode, &d1Decode)
if err != nil {
log.Fatal(err.Error())
}
fmt.Println(d1Decode.GetName())
fmt.Println(d1Decode.GetAge())
fmt.Println(d1Decode.GetInfo().GetG())
}
go run main.go
[10 3 116 111 109 16 99 26 15 10 7 98 101 105 106 105 110 103 18 4 99 111 100 101]
tom
99
MALE
Protobuf 基本使用强烈推荐看看官方文档 :
https://developers.google.com/protocol-buffers/docs/gotutorial
4. Protobuf 协议语法
我们注意到编写的 .proto
文件是按特定的语法格式编写的
- Protobuf的语法相对很简单
- Protobuf的语目前
proto2
和proto3
两个版本- 如果项目无历史负担,强烈推荐使用 proto3 的语法编写
.proto
文件
5. Protobuf与RPC
我们将Protobuf 和RPC 结合起来做一个简单的Demo
步骤如下 :
- 编写
.proto
文件- 自动生成代码
.pb.go
的代码文件- 编写 RPC的服务端
- 编写 PRC的客户端
- 运行测试
目录结构如下
| |___example |______demo3.proto |______demo3.pb.go |___main |______demo3_server.go |______demo3_client.go
编写
demo3.proto
文件
syntax = "proto3"; // 使用 protobuf3的语法
package example;
message Demo3Request {
int64 id = 1;
}
message Demo3Response {
string name = 1;
int32 age = 2;
enum Gender {
MALE = 0;
FEMALE = 1;
UNKNOWN = 2;
}
message Other {
string addr = 1;
string hobby = 2;
Gender g = 3;
}
Other info = 3;
}
生成
.pb.go
文件
protoc --go_out=. demo3.proto
编写 rpc服务端文件
demo3_server.go
package main
import (
"GoNote/chapter10/demo9/example"
"github.com/pkg/errors"
"log"
"net/http"
"net/rpc"
)
type Demo3Service struct {
}
func (d *Demo3Service) GetUser(request example.Demo3Request, response *example.Demo3Response) error {
// 模拟数据
// 数据获取逻辑自行设计
datas := map[int64]example.Demo3Response{
1: {Name: "AAA", Age: 999, Info: &example.Demo3Response_Other{Addr: "beijing", Hobby: "sport", G: example.Demo3Response_MALE}},
2: {Name: "BBB", Age: 888, Info: &example.Demo3Response_Other{Addr: "上海", Hobby: "sport", G: example.Demo3Response_FEMALE}},
3: {Name: "CCC", Age: 777, Info: &example.Demo3Response_Other{Addr: "wuhan", Hobby: "sport", G: example.Demo3Response_UNKNOWN}},
4: {Name: "DDD", Age: 666, Info: &example.Demo3Response_Other{Addr: "重庆", Hobby: "sport", G: example.Demo3Response_MALE}},
5: {Name: "EEE", Age: 555, Info: &example.Demo3Response_Other{Addr: "", Hobby: "sport", G: example.Demo3Response_FEMALE}},
}
// 模拟业务处理逻辑
if value, ok := datas[request.GetId()]; ok {
*response = value
} else {
return errors.New("not found")
}
return nil
}
func main() {
rpc.Register(new(Demo3Service))
rpc.HandleHTTP()
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Println(err.Error())
}
}
编写 rpc 客户端文件
demo3_client.go
package main
import (
"GoNote/chapter10/demo9/example"
"fmt"
"log"
"net/rpc"
)
func main() {
client, err := rpc.DialHTTP("tcp", ":8080")
if err != nil {
log.Fatal(err.Error())
}
request := example.Demo3Request{Id: 1}
var response example.Demo3Response
err = client.Call("Demo3Service.GetUser", request, &response)
if err != nil {
log.Fatal(err.Error())
}
fmt.Println(response.GetName())
fmt.Println(response.GetInfo().GetAddr())
}
运行
demo3_server.go
和demo3_client.go
AAA
beijing