1.插件命名规则
proto插件名称需要使用protoc-gen-xxx
当使用protoc –xxx_out时就会调用proto-gen-xxx插件
2.protobuf解析一般流程
方法1:
- 先通过标准输入生成CodeGeneratorRequest
- 通过CodeGeneratorRequest初始化插件plugin
- 通过插件plugin获取文件File
- 遍历文件,处理内容,生成文件
- 像标准输出写入响应plugin.Response
示例代码:
s := flag.String("a", "", "a")
flag.Parse()
//生成Request
input, _ := ioutil.ReadAll(os.Stdin)
var req pluginpb.CodeGeneratorRequest
proto.Unmarshal(input, &req)
//设置参数,生成plugin
opts := protogen.Options{
ParamFunc: flag.CommandLine.Set,
}
plugin, err := opts.New(&req)
if err != nil {
panic(err)
}
fmt.Fprintf(os.Stderr, "a=%s\n", *s)
// protoc 将一组文件结构传递给程序处理,包含proto中import中的文件
for _, file := range plugin.Files {
if !file.Generate { //显示传入的文件为true
continue
}
fmt.Fprintf(os.Stderr, "path:%s\n", file.GoImportPath)
genF := plugin.NewGeneratedFile(fmt.Sprintf("%s_error.pb.go", file.GeneratedFilenamePrefix), file.GoImportPath) //用来处理生成文件的对象
GenFile(genF, file, *s)
}
// 生成response
resp := plugin.Response()
out, err := proto.Marshal(resp)
if err != nil {
panic(err)
}
// 相应输出到stdout, 它将被 protoc 接收
fmt.Fprintf(os.Stdout, string(out))
方法2:
var s = flag.String("aaa", "", "aaa")
var s1 = flag.String("bbb", "", "aaa")
flag.Parse()
protogen.Options{
ParamFunc: flag.CommandLine.Set, //设置命令行参数 --xxx_out=aaa=10,bbb=20:.
}.Run(func(gen *protogen.Plugin) error { //Run内部封装了方法1的步骤
for _, f := range gen.Files {
if !f.Generate { //判断是否需要生成代码, 影响这里是否是true的原因是protoc是否指定这个文件
continue
}
//遍历各种message/service...,同方法1
}
})
示例代码:
protogen.Options{
ParamFunc: flag.CommandLine.Set,
}.Run(func(gen *protogen.Plugin) error { //Run中已经封装好request和response
fmt.Fprintf(os.Stderr, "aaa=%s\n", *s)
fmt.Fprintf(os.Stderr, "bbb=%s\n", *s1)
for _, f := range gen.Files {
fmt.Fprintf(os.Stderr, "f.Generate:%s==>%v\n", f.Desc.Name(), f.Generate)
if !f.Generate { //判断是否需要生成代码
continue
}
for _ ,file := range gen.Files {
for _, serv := range file.Services {
fmt.Fprintf(os.Stderr, "%s ==> %s\n", serv.Desc.Name(), serv.GoName)
}
}
}
return nil
})
注意打印信息:不能打印在标准输出因为程序响应返回要占用标准输出传递到父进程protoc, 要想调试打印到标准错误上
注意:
- 参数传递方式为
--xxx_out=aaa=10,bbb=20:.
- 通过GeneratedFile生成的文件必须符合go语法否则报错
一些细节
导包自动重名
//导入包
//gen *protogen.Plugin
//protogen.GoImportPath("errors") 把errors转换成GoImportPath类型
//protogen.GoImportPath("errors").Ident("") 返回里面变量名或函数名等, 为空时返回包名
//例如:
//errors = protogen.GoImportPath("errors")
//errors.Ident("A") errors包中A的标识, errors.A
//errors.Ident("") errors包名 errors
//errors.Ident("New(\"a\")") -->生成errors.New("a")
//genF.QualifiedGoIdent(b) 虽然有导入包和返回合法名称的功能,如果GoIdent直接传入P函数会在P内部调用QualifiedGoIdent,也就是说会自动导包,并且用合法的名称
b := protogen.GoIdent{ //代表a/b包下的b
GoName: "b",
GoImportPath: "a/b",
}
name := genF.QualifiedGoIdent(b) //自动导入a/b包,并返回名称b.b
fmt.Fprintf(os.Stderr, "b:%s\n", name)
c := protogen.GoIdent{
GoName: "b",
GoImportPath: "c/b",
}
name = genF.QualifiedGoIdent(c) //自动导入c/b包,并返回名称, 这个包和上面名称都是b, 这个会返回b1.b
fmt.Fprintf(os.Stderr, "c:%s\n", name)
//所以说如果使用的是P函数生成代码,一般不会手动调用QualifiedGoIdent, 如果使用的是text模板生成代码,就需要自己手动调用QualifiedGoIdent来生成字符串名称了
结论: 所以说如果使用的是P函数生成代码,不需要手动调用QualifiedGoIdent, P函数会判断如果是GoIdent类型会自动调用QualifiedGoIdent, 如果使用的是text模板生成代码,就需要自己手动调用QualifiedGoIdent来生成字符串名称了
影响file.Generate的值是否为true 的原因是protoc是否指定这个文件
比如 A.proto 定义了一些枚举, B.proto import了A, 在执行protoc时 使用了protoc --xxx_out=. B.proto
这时gen.Files中会包含import的文件A, 但是因为protoc只指定了B,那么A.Generate=false, B.Generate=true
3.protobuf的扩展
go(别的语言没研究过)中protobuf解析是通过proto自描述的,描述文件在(google/protobuf/descriptor.proto)
例如google.protobuf.FieldOptions
是描述字段选项,通过扩展该message描述可实现高级功能
注意:proto3语法只能进行扩展(extend)自定义选项, 不支持extensions关键字
extend google.protobuf.FieldOptions {
optional float my_field_option = 50002; //数字需要填写FieldOptions extensions指定的范围,不用修改源文件即可实现扩展
}
for _, msg := range file.Proto.MessageType { //遍历proto文件中所有message消息体
for _, field := range msg.Field { //遍历message字段
opt := proto.GetExtension(field.Options, aaa.E_MyFieldOption)//获取字段选项,上述proto文件中扩展字段, aaa.E_MyFieldOption是通过protoc -go_out 输出的文件,扩展字段都是以E开头大驼峰
if v, ok := opt.(float32); ok { //如果扩展是message的话一般是转换成指针,可以点开生成的扩展go文件查看ExtensionType属性
fmt.Fprintf(&buf, "//%s=%s=%f\n", *msg.Name, *field.Name, v)
}
}
}
protobuf扩展语法(https://developers.google.com/protocol-buffers/docs/overview#customoptions)
import "google/protobuf/descriptor.proto";
extend google.protobuf.FileOptions { //文件选项扩展
optional string my_file_option = 50000;
}
extend google.protobuf.MessageOptions { //message选项扩展
optional int32 my_message_option = 50001;
}
extend google.protobuf.FieldOptions { //字段选项扩展
optional float my_field_option = 50002;
}
extend google.protobuf.EnumOptions { //枚举选项扩展
optional bool my_enum_option = 50003;
}
extend google.protobuf.EnumValueOptions { //枚举值(类似field)选项扩展
optional uint32 my_enum_value_option = 50004;
}
extend google.protobuf.ServiceOptions { //service选项扩展
optional MyEnum my_service_option = 50005;
}
extend google.protobuf.MethodOptions { //方法选项扩展
optional MyMessage my_method_option = 50006;
}
option (my_file_option) = "Hello world!"; //使用文件选项
message MyMessage {
option (my_message_option) = 1234; //使用message选项
int32 foo = 1 [(my_field_option) = 4.5]; //使用field选项
string bar = 2;
}
enum MyEnum {
option (my_enum_option) = true; //枚举选项
FOO = 1 [(my_enum_value_option) = 321]; //枚举值选项
BAR = 2;
}
message RequestType {}
message ResponseType {}
service MyService {
option (my_service_option) = FOO; //服务选项
rpc MyMethod(RequestType) returns(ResponseType) {
// Note: my_method_option has type MyMessage. We can set each field
// within it using a separate "option" line.
option (my_method_option).foo = 567; //方法选项
option (my_method_option).bar = "Some string";
//或者
option (my_method_option) = {
foo:123,
bar:"123123"
}
}
}