protobuf就是一种数据格式,如json,xml等
1.安装proto工具,并copy到path/bin下 https://github.com/protocolbuffers/protobuf/releases
2.go get -u github.com/golang/protobuf/protoc-gen-go
protoc-gen-go在两个地方都有
https://github.com/golang/protobuf/tree/master/protoc-gen-go
https://github.com/protocolbuffers/protobuf-go/tree/master/cmd/protoc-gen-go
protoc 和 protoc-gen-go 产生的 proto 文件代码对比
$ protoc —go_out=./go/ ./proto/helloworld.proto
$ protoc —go_out=plugins=grpc:./go2/ ./proto/helloworld.proto
上面两个命令,第一个产生的只是 protobuffer 文件序列化和反序列化的代码。
第二个产生的则除了第一个的代码外,还增加服务器和客户端通讯、实现的公共库代码。
走通流程
1.编写proto文件, person.proto
syntax = "proto3"; //首行声明使用的protobuf版本为proto3
package serialize; // 声明所在包
// package 项目名.模块名; // 常见的命名
option go_package = "domain.com/projectName/serialize;proto"; // 生成的go文件包名
// message person 会生成 Person 命名的结构体
message person {
int32 id = 1;
string name = 2;
}
// message all_person 会按照驼峰规则自动生成名为AllPerson 的结构体
message all_person {
repeated person Per = 1;
}
//--------------枚举-----------------------
一般的枚举的第一个字段使用
enum PhoneType {
UNSPECIFIED = 0; // 未使用,第一个必须是0
MOBILE = 1;
}
编译为go文件就是
type PhoneType int32
const (
PhoneType_UNSPECIFIED PhoneType = 0 // 未使用,第一个必须是0
PhoneType_MOBILE PhoneType = 1 //第一个必须是0
)
func (x Order_State) Enum() *Order_State {
p := new(Order_State)
*p = x
return p
}
func (x PhoneType) String() string {
return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
}
func (PhoneType) Descriptor() protoreflect.EnumDescriptor {
return file_order_proto_order_proto_enumTypes[4].Descriptor()
}
func (PhoneType) Type() protoreflect.EnumType {
return &file_order_proto_order_proto_enumTypes[4]
}
func (x PhoneType) Number() protoreflect.EnumNumber {
return protoreflect.EnumNumber(x)
}
// Deprecated: Use Order_State.Descriptor instead.
func (PhoneType) EnumDescriptor() ([]byte, []int) {
return file_order_proto_order_proto_rawDescGZIP(), []int{1, 1}
}
// 枚举类型还会生成两个字典
var (
PhoneType_name = map[int32]string{
0: "UNSPECIFIED",
1: "MOBILE",
}
PhoneType_value = map[string]int32{
"UNSPECIFIED": 0,
"MOBILE": 1,
}
)
-----------------------------------------------
message MyMessage1 {
enum EnumAllowingAlias {
option allow_alias = true; // 如果枚举有相同的值,设置allow_alias = true,否则报错
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
type MyMessage1_EnumAllowingAlias int32
const (
MyMessage1_UNKNOWN MyMessage1_EnumAllowingAlias = 0
MyMessage1_STARTED MyMessage1_EnumAllowingAlias = 1
MyMessage1_RUNNING MyMessage1_EnumAllowingAlias = 1
)
//--------------嵌套结构体-----------------------
如果要引用内部类,则通过parent.type方式来调用
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
required int64 ival = 1;
optional bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
required int32 ival = 1;
}
}
}
//--------------Maps-----------------------
map<key_type, value_type> map_field = N;
key_type可以是除浮点指针或bytes外的其他基本类型,value_type可以是任意类型
map<string, Project> projects = 3;
map 类型的字段不可重复(不能用 repeated 修饰)
//--------------reserved---------------------
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
如果通过完全删除某个字段或对其进行注释来更新消息类型,则将来的用户可以在对该类型进行自己的更新时重用该
字段编号。如果以后加载相同.proto的旧版本,这可能会导致严重问题,包括数据损坏、隐私漏洞等。确保不会发生
这种情况的一种方法是指定保留已删除字段的字段号(和/或名称,这也可能导致JSON序列化问题)。
//--------------deprecated--------------------- 弃用
message FooResponse {
repeated Order orders = 1 [ deprecated = true ];
int32 id = 2;
}
//--------------json_name---------------------
message FooResponse {
repeated Order orders = 1 [ json_name = "Order" ];
int32 id = 2;
}
- 进入proto 目录,使用 protoc 编译 person.proto
- protoc —go_out=. person.proto
- 执行完后会生成一个 person.pb.go 文件
3.使用
package main
import (
"fmt"
"github.com/golang/protobuf/proto"
"io/ioutil"
"study/grpc/protobuf/serializa"
)
func main() {
var p1 serialize.Person
p1.Id=1
p1.Name="tom"
var p2 serialize.Person
p2.Id=2
p2.Name="jack"
all_p := serialize.AllPerson{Per:[]*serialize.Person{&p1,&p2}}
// 对数据进行序列化
//注意参数一定要是指针类型,AllPerson实现了message接口,
//在message内部就拿不到AllPerson的地址了
data,err :=proto.Marshal(&all_p)
if err !=nil{
fmt.Println("error:",err.Error())
return
}
ioutil.WriteFile("proto.dat",data,0777)
data2,err :=ioutil.ReadFile("proto.dat")
if err !=nil{
fmt.Println("error:",err.Error())
return
}
// 对数据进行反序列化
var tag serialize.AllPerson
proto.Unmarshal(data2,&tag)
for _,v:=range tag.Per{
fmt.Println(v)
}
}
//---------------------------------
id:1 name:"tom"
id:2 name:"jack"
生成的proto.dat
数据类型
语法
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 语法说明(syntax)前只能是空行或者注释
- 每个字段由【字段限制】、字段类型、字段名和编号四部分组成
字段限制
required
:必须赋值的字段,proto3已不支持optional
:可有可无的字段,改名为 “singular”,默认就是这个,不能显示的添加该限制repeated
:可重复字段
编号
- 身份编号,1-2^{29}-1,但不能使用19000到19999之间的值
导包
import搜索路径, 在使用protoc编译时,需要使用选项-I或—proto_path通知protoc去什么地方查找import的文件,如果不指定,protoc将会在当前目录(即调用protoc的路径)下查找。
import "xx/xx/xx.proto":导入其他的包,这样你就可以直接使用其他的包的数据结构
例如:xx/xx/a.proto 定义了message AAA
在xx/xx/b.proto 中可直接使用a.AAA
如果b.proto 与a.proto的package一致,在b.proto 直接使用AAA(而不是a.AAA)
注意:例如现在有3个文件:a,b,c,b导入了a,c导入了b,c是不能拿到a的数据结构,解决:b再导入a时,使用 import public “a.proto” 就可以了
protoc参数
Usage: protoc [OPTION] PROTO_FILES
- -I 如果多个proto文件之间有互相依赖,生成某个proto文件时,需要import其他几个proto文件,这时候就要用-I来指定搜索目录,如果没有指定,则在当前目录将座位搜索目录
- —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.
- —javanano_out=OUT_DIR Generate Java Nano source file.
- —js_out=OUT_DIR Generate JavaScript source.
- —objc_out=OUT_DIR Generate Objective C header and source.
- —python_out=OUT_DIR Generate Python source file.
- —ruby_out=OUT_DIR Generate Ruby source file.
举例
项目结构:
AAAAA
common
common.proto
common.pb.go
pubplan
pubplan.proto
cd 到AAAAA的上一级目录 执行:
- protoc AAAAA/pubplan/pubplan.proto —go_out=plugins=grpc,paths=source_relative:.
- 会在AAAAA/pubplan下生成go文件,并且
common.proto
syntax = "proto3";
package common;
//ssh直连信息
message SshInfoStruct {
string User = 1;
string PassWord = 2;
string Host = 3;
int64 Port = 4;
string KeyPath = 5;
}
// 通用请求体
message Request {
string cmd = 1;
SshInfoStruct info = 2;
}
pubplan.proto
syntax = "proto3";
package pubplan;
import "AAAAA/common/common.proto";
message PlanAppPubReq{
common.SshInfoStruct info = 1;
string appName = 2;
string middlewareName = 3;
int64 pubMode = 4;
string extParam = 5;
string targetPath = 6;
string serverNode = 7;
string ecsTempPath = 8;
string ecsBakPath = 9;
string fileName = 10;
string appProcessName = 11;
bool isRepair = 12;
}
其它
Any
Any消息类型允许您将消息作为嵌入类型使用,而无需使用它们的.proto定义。Any包含一个以字节表示的任意序列化消息,以及一个URL,该URL充当该消息的全局唯一标识符并解析为该消息的类型。
import "google/protobuf/any.proto";
message Response {
uint32 Code = 1;
string Msg = 2;
google.protobuf.Any data = 3;
}
生成的结构体
type Response struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Code uint32 `protobuf:"varint,1,opt,name=Code,proto3" json:"Code,omitempty"`
Msg string `protobuf:"bytes,2,opt,name=Msg,proto3" json:"Msg,omitempty"`
Data *any.Any `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
}
测试
package main
import (
"fmt"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
myprto "commons/grpc2/proto"
)
func main() {
marshal := &myprto.TestAny{
Id: 1,
Title: "标题",
Content: "内容",
}
any, err := anypb.New(marshal)
fmt.Println(any, err) // [type.googleapis.com/rpc.TestAny]:{Id:1 Title:"标题" Content:"内容"} <nil>
msg := &myprto.Response{
Code: 0,
Msg: "success",
Data: any,
}
fmt.Println(msg) // Msg:"success" data:{[type.googleapis.com/rpc.TestAny]:{Id:1 Title:"标题" Content:"内容"}}
unmarshal := &myprto.TestAny{}
err = anypb.UnmarshalTo(msg.Data, unmarshal, proto.UnmarshalOptions{})
fmt.Println(unmarshal, err) // Id:1 Title:"标题" Content:"内容" <nil>
}
调整默认tag
- protoc-go-inject-tag: https://github.com/favadi/protoc-go-inject-tag
- 安装:go get github.com/favadi/protoc-go-inject-tag
这个库可以再proto文件中注入tag,然后在导出的时候相应的字段的tag就可以被修改掉了。
message MyMessage {
// @inject_tag: json:"Code"
int64 Code = 1;
}
生成普通的存根文件后,再执行
protoc-go-inject-tag -input=./test.pb.go
生成的结构体
type MyMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
// @inject_tag: json:"Code"
Code int64 `protobuf:"varint,1,opt,name=Code,proto3" json:"Code"`
}
extend
proto3 不支持extension,只支持option, option传值设置:
https://developers.google.com/protocol-buffers/docs/proto3#options
具体option可以分为:
- FileOptions
- MessageOptions
- FieldOptions
- OneofOptions
- EnumOptions
- EnumValueOptions
- ServiceOptions
- MethodOptions
- UninterpretedOption
详见:https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/descriptor.proto
作用:见字意为一个扩展,详细的可以理解是为结构体添加一个或多个字段,但是不能显示的获取或设置值。下面为MessageOptions的一个例子
test.proto
syntax = "proto3";
package protobuf;
import "google/protobuf/descriptor.proto";
option go_package = "protobuf;protobuf";
// extend 编号范围为50000-99999
extend google.protobuf.MessageOptions {
string AAA = 50000 ;
uint64 BBB = 50001 ;
}
test1.proto
syntax = "proto3";
package protobuf;
import "grpc/protobuf/test.proto";
message MyPerson {
option (AAA) = "aaa";
string BBB = 1;
}
拿到option字段:
package main
import (
pb "commons/grpc/protobuf"
"google.golang.org/protobuf/descriptor"
"google.golang.org/protobuf/proto"
"log"
)
func main() {
p := pb.MyPerson{}
_, pd := descriptor.MessageDescriptorProto(&p)
if v, err := proto.GetExtension(pd.Options, pb.E_AAA); err == nil {
log.Printf("GetExtensions(pf.Options, pb.E_AAA) file_opt1:%v", *(v.(*string)))
} else {
log.Fatalf("GetExtensions(pf.Options, pb.E_AAA) failed: %v", err)
}
newv := "aaa_reset"
if err := proto.SetExtension(pd.Options, pb.E_AAA, &newv); err != nil {
log.Fatalf("SetExtension(pf.Options, pb.E_AAA) failed: %v", err)
}
if v, err := proto.GetExtension(pd.Options, pb.E_AAA); err == nil {
log.Printf("GetExtensions(pf.Options, pb.E_AAA) file_opt1:%v", *(v.(*string)))
} else {
log.Fatalf("GetExtensions(pf.Options, pb.E_AAA) failed: %v", err)
}
}
2021/02/28 22:38:30 GetExtensions(pf.Options, pb.E_AAA) file_opt1:aaa
2021/02/28 22:38:30 GetExtensions(pf.Options, pb.E_AAA) file_opt1:aaa_reset
JSON 映射
Proto3 支持JSON的编码规范,使他更容易在不同系统之间共享数据,在下表中逐个描述类型。
如果JSON编码的数据丢失或者其本身就是null,这个数据会在解析成protocol buffer的时候被表示成默认值。如果一个字段在protocol buffer中表示为默认值,体会在转化成JSON的时候编码的时候忽略掉以节省空间。具体实现可以提供在JSON编码中可选的默认值。
proto3 | JSON | JSON示例 | 注意 |
---|---|---|---|
message | object | {“fBar”: v, “g”: null, …} | 产生JSON对象,消息字段名可以被映射成lowerCamelCase形式,并且成为JSON对象键,null被接受并成为对应字段的默认值 |
enum | string | “FOO_BAR” | 枚举值的名字在proto文件中被指定 |
map | object | {“k”: v, …} | 所有的键都被转换成string |
repeated V | array | [v, …] | null被视为空列表 |
bool | true, false | true, false | |
string | string | “Hello World!” | |
bytes | base64 string | “YWJjMTIzIT8kKiYoKSctPUB+” | |
int32, fixed32, uint32 | number | 1, -10, 0 | JSON值会是一个十进制数,数值型或者string类型都会接受 |
int64, fixed64, uint64 | string | “1”, “-10” | JSON值会是一个十进制数,数值型或者string类型都会接受 |
float, double | number | 1.1, -10.0, 0, “NaN”, “Infinity” | JSON值会是一个数字或者一个指定的字符串如”NaN”,”infinity”或者”-Infinity”,数值型或者字符串都是可接受的,指数符号也可以接受 |
Any | object | {“@type”: “url”, “f”: v, … } | 如果一个Any保留一个特上述的JSON映射,则它会转换成一个如下形式:{“@type”: xxx, “value”: yyy}否则,该值会被转换成一个JSON对象,@type字段会被插入所指定的确定的值 |
Timestamp | string | “1972-01-01T10:00:20.021Z” | 使用RFC 339,其中生成的输出将始终是Z-归一化啊的,并且使用0,3,6或者9位小数 |
Duration | string | “1.000340012s”, “1s” | 生成的输出总是0,3,6或者9位小数,具体依赖于所需要的精度,接受所有可以转换为纳秒级的精度 |
Struct | object | { … } | 任意的JSON对象,见struct.proto |
Wrapper types | various types | 2, “2”, “foo”, true, “true”, null, 0, … | 包装器在JSON中的表示方式类似于基本类型,但是允许nulll,并且在转换的过程中保留null |
FieldMask | string | “f.fooBar,h” | 见fieldmask.proto |
ListValue | array | [foo, bar, …] | |
Value | value | 任意JSON值 | |
NullValue | null | JSON null |
any的例子
package google.profile;
message Person {
string first_name = 1;
string last_name = 2;
}
{
"@type": "type.googleapis.com/google.profile.Person",
"firstName": <string>,
"lastName": <string>
}
引入 google 定义的 pb 出错
先将protobuf 下载后放 gopath src 下
再配置 goland