Protocol Buffers(简称:ProtoBuf)是一种序列化数据结构的协议。对于透过管道(pipeline)或存储资料进行通信的程序开发上是很有用的。这个方法包含一个接口描述语言,描述一些数据结构,并提供程序工具根据这些描述产生代码,用于将这些数据结构产生或解析资料流。——维基百科

protoc生成工具:here

ProtoBuf支持很多语言,生成每种语言可使用的协议代码则是另一个类似于插件一样的工具,Go则类似于有protoc-gen-go

之前也对Go-Micro的gen-micro比较感兴趣,也正好在找寻一种方便生成代码的办法。

插件的运行流程

在执行protoc编译的时候,命令行是这样写的:protoc --proto_path=. --go_out==paths=source_relative:. hello.protoprotoc会去读取hello.proto,然后将其编译成.go的文件(要求PATH下有protoc-gen-go工具)。

如果我们命令行这样写:protoc --xxx_out=./ *.proto,由于不认识xxx,于是protoc会在PATH路径下寻找一个叫做protoc-gen-xxx的可执行文件。而protoc-gen-xxx就是我们要实现的插件。

插件的运行流程如图:

  • 我们只需要关注步骤3.1~3.5,其余步骤是谷歌protoc完成
  • CodeGeneratorRequest和CodeGeneratorResponse对象定义在plugin.proto里面
  • FileDescriptor代表了一个proto文件,定义在descriptor.proto里面

protoc执行过程

参考:Protobuf第三方扩展开发指南

Error插件

在之前我有关于Error的文章中,就有所提及error有着常见的几种表现形式,特别是关于在GRPC中怎么处理error的实践。这种处理我觉得应该是Go-Micro中先使用的,然后由Kratos中继续演进,我是非常喜欢Kratos对error的进一步简化的,这其中就包括我所看中的Error protoc插件。

1
2
3
4
5
6
7
8
main()
——> generateFile(gen, f)
调用g.P("// Code generated by protoc-gen-go-errors. DO NOT EDIT.")
func (g *GeneratedFile) P(v ...interface{})
打印数据类似于printf重定向到文件中
——> generateFileContent(gen, file, g)
——> 仅从protoc传入文件解析中拿取枚举值
——> genErrorsReason,利用Go中的template填充代码参数,然后打印出来

此处还借用了google带的两个扩展参数:google.protobuf.EnumOptionsgoogle.protobuf.EnumValueOptions

EnumOptions前者是定义了一个可选字段——default_code

EnumValueOptions则是扩展了error的注释;

1
2
3
4
defaultCode := proto.GetExtension(enum.Desc.Options(), errors.E_DefaultCode)
......

eCode := proto.GetExtension(v.Desc.Options(), errors.E_Code)

查看proto文件,如下,error.proto是需要的,且只能这么生成,需要和工具的字段对应上,否则,error_reason.proto将用不了default_code和code两个扩展功能。

error.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
syntax = "proto3";

package errors;

option go_package = "github.com/go-kratos/kratos/v2/errors;v1";

import "google/protobuf/descriptor.proto";

extend google.protobuf.EnumOptions {
int32 default_code = 1108;
}

extend google.protobuf.EnumValueOptions {
int32 code = 1109;
}

error_reason.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
syntax = "proto3";

package errors;
import "errors.proto";

option go_package = "github.com/go-kratos/kratos/examples/blog/api/v1;v1";

enum ErrorReason {
option (errors.default_code) = 500;

USER_NOT_FOUND = 0 [(errors.code) = 404];
CONTENT_MISSING = 1 [(errors.code) = 400];
CONTENT2 = 3;
}

生成的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func IsContentMissing(err error) bool {
e := errors.FromError(err)
return e.Reason == ErrorReason_CONTENT_MISSING.String() && e.Code == 400
}

func ErrorContentMissing(format string, args ...interface{}) *errors.Error {
return errors.New(400, ErrorReason_CONTENT_MISSING.String(), fmt.Sprintf(format, args...))
}

func IsCONTENT2(err error) bool {
e := errors.FromError(err)
return e.Reason == ErrorReason_CONTENT2.String() && e.Code == 500
}

func ErrorCONTENT2(format string, args ...interface{}) *errors.Error {
return errors.New(500, ErrorReason_CONTENT2.String(), fmt.Sprintf(format, args...))
}

可以看到最终的结果是这样的不透明错误处理函数,也就是之前说的Opaque Error。别人引用的时候就可以直接调用这些处理函数了,很方便。