Protocol Buffers 是谷歌推出的编码标准,它在传输效率和编解码性能上都要优于 JSON。但其代价则是需要依赖中间描述语言(IDL)来定义数据(和服务)的结构(通过 *.proto 文件),并且需要一整套的工具链(protoc 及其插件)来生成对应的序列化和反序列化代码。
除了谷歌官方提供的工具和插件(比如生成 go 代码的 protoc-gen-go)外,我们还可以开发或定制自己的插件,根据业务需要按照 proto 文件的定义生成代码或者文档。
1.1开发流程
开发proto plugin可以分为三步:
从 stdin 读取proto文件(ex: test.proto)
根据proto文件自定义规则构造目标文件结构 (ex: main.go)
将生成的文件通过 stdout 输出(ex: test.pb.go)
example
main.go
func main() {
m := myProtoPlugin{}
protogen.Options{}.Run(m.Generate)
}
type myProtoPlugin struct {
}
//实现Generate方法
func (m myProtoPlugin) Generate(plugin *protogen.Plugin) error {
if len(plugin.Files) < 1 {
return nil
}
f := plugin.Files[len(plugin.Files)-1]
//指定输出文件名,可以是任何自定义的文件格式
fileName := f.GeneratedFilenamePrefix + “.pb.go”
//构造目标文件
generatedFile := plugin.NewGeneratedFile(fileName, f.GoImportPath)
//向目标文件里填充内容
generatedFile.P(“// this file is generated by proto file.”)
generatedFile.P(“package main;”)
_ = generateStructFromMessage(generatedFile, f) return nil
}
func generateStructFromMessage(genFile *protogen.GeneratedFile, protoFile *protogen.File) error {
messages := protoFile.Messages
for _, message := range messages {
print(genFile.QualifiedGoIdent(message.GoIdent))
genFile.P(“type “, message.GoIdent.GoName, " struct{”)
for _, field := range message.Fields {
genFile.P(” “, field.GoName, " “, field.Desc.Kind(), “;”)
}
genFile.P(”}”)
_ = generateGetterMethod(genFile, message)
}
return nil
}
func generateGetterMethod(genFile *protogen.GeneratedFile, message *protogen.Message) error {
fields := message.Fields
for _, field := range fields {
genFile.P(“func (m *”, message.GoIdent.GoName, “) Get”, field.GoName, “() “, field.Desc.Kind(), “{”)
genFile.P(” return m.”, field.GoName)
genFile.P(“}”)
}
return nil
}
test.proto
syntax = “proto2”;
import “google/protobuf/descriptor.proto”;
import “validate.proto”;
package main;
option go_package = “./”;
message test {
optional string a = 1;
}
message People {
optional string name = 1;
optional string phone = 2;
}
test.pb.go
// this file is generated by proto file.
package main
type Test struct {
A string
}
func (m *Test) GetA() string {
return m.A
}
type People struct {
Name string
Phone string
}
func (m *People) GetName() string {
return m.Name
}
func (m *People) GetPhone() string {
return m.Phone
}
命令:
go build main.go && protoc --plugin=protoc-gen-myProtoPlugin=main --myProtoPlugin_out=./ test.proto
执行流程
func run(opts Options, f func(*Plugin) error) error {
if len(os.Args) > 1 {
return fmt.Errorf(“unknown argument %q (this program should be run by protoc, not directly)”, os.Args[1])
}
// 读入proto文件
in, err := ioutil.ReadAll(os.Stdin)
if err != nil {
return err
}
req := &pluginpb.CodeGeneratorRequest{}
// 序列化为GodeGeneratorRequest
if err := proto.Unmarshal(in, req); err != nil {
return err
}
// 将GodeGeneratorRequest转化为Plugin
gen, err := opts.New(req)
if err != nil {
return err
}
// 执行自定义的方法,构造输出文件格式以及内容
if err := f(gen); err != nil {
gen.Error(err)
}
resp := gen.Response()
// 序列化目标文件
out, err := proto.Marshal(resp)
if err != nil {
return err
}
// 输出目标文件
if _, err := os.Stdout.Write(out); err != nil {
return err
}
return nil
}
type CodeGeneratorRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
FileToGenerate []string protobuf:"bytes,1,rep,name=file_to_generate,json=fileToGenerate" json:"file_to_generate,omitempty"
Parameter *string protobuf:"bytes,2,opt,name=parameter" json:"parameter,omitempty"
ProtoFile []*descriptorpb.FileDescriptorProto protobuf:"bytes,15,rep,name=proto_file,json=protoFile" json:"proto_file,omitempty"
CompilerVersion *Version protobuf:"bytes,3,opt,name=compiler_version,json=compilerVersion" json:"compiler_version,omitempty"
}
type CodeGeneratorResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Error *string protobuf:"bytes,1,opt,name=error" json:"error,omitempty"
SupportedFeatures *uint64 protobuf:"varint,2,opt,name=supported_features,json=supportedFeatures" json:"supported_features,omitempty"
File []*CodeGeneratorResponse_File protobuf:"bytes,15,rep,name=file" json:"file,omitempty"
}
生成的主要对象
Options
除了通过定义一般的message生成目标文件,还可以通过为field或者message甚至file定义扩展属性extension来实现更加灵活、丰富的操作以满足复杂的需求。
例如,可以通过为message的filed加上options来来实现field validator的功能,如下:
通过为phone添加stringValidate option来实现限制phone的长度
message StringFieldRules {
optional int32 min_len = 1;
optional int32 max_len = 3;
}
message People {
optional string name = 1;
optional string phone = 2 [(validate.stringValidated)= {
min_len: 10, max_len: 20;
}];
}
以用field的option构造field validator插件为例,开发步骤可以分为以下几步:
构造validate.proto文件用以描述validator的规则,同时生成valiate.pb.go文件
在目标proto文件里引入validate.proto文件,在想要限定的field上加上自定义的规则,如字符长度,格式等
按照1.1的流程对field的option进行解析,获取option相关属性,如定义的min_len, max_len,然后按照自定义规则进行逻辑编码,生成validator文件
validate.proto
message StringFieldRules {
optional int32 min_len = 1;
optional int32 max_len = 3;
}
extend google.protobuf.FieldOptions {
optional StringFieldRules stringValidated = 5000;
}
validator的generate
func generateValidated(genFile *protogen.GeneratedFile, protoFile *protogen.File) error {
messages := protoFile.Messages
for _, message := range messages {
for _, field := range message.Fields {
// 获取field的options
fieldOptions := field.Desc.Options().(*descriptorpb.FieldOptions)
if fieldOptions == nil || fieldOptions.ProtoReflect() == nil {
continue
}
genFile.P(“func (m *”, message.GoIdent.GoName, “) Validated”, field.GoName, “() bool”, “{”)
// 处理string类型的valdator
if field.Desc.Kind() == protoreflect.StringKind {
ext := proto.GetExtension(fieldOptions, validate.E_StringValidated).(*validate.StringFieldRules)
if ext == nil {
continue
}
min := *ext.MinLen
max := *ext.MaxLen
genFile.P(“if len(m.”, field.GoName, “)< “, min, “|| len(m.”, field.GoName, “)>”, max, “{”)
genFile.P(” return false”)
genFile.P(“}”)
genFile.P(“return true”)
genFile.P(“}”)
}
}
}
return nil
}
生成的validator
func (m *People) ValidatedPhone() bool {
if len(m.Phone) < 10 || len(m.Phone) > 20 {
return false
}
return true
}