当前位置: 首页 > 工具软件 > code-gen > 使用案例 >

protoc-gen-go 介绍与源代码分析

江德润
2023-12-01

protoc-gen-go

github 地址: https://github.com/golang/protobuf/tree/master/protoc-gen-go

它是 protoc 的一个插件,通过它, golang/protobuf 使 proto 定义文件,生成 golang 版本协议代码

protoc-gen-go 具有良好的代码结构,可以简单在 protoc-gen-go 代码基础上,新增(不需要改 protoc-gen-go 代码)自己的适配逻辑

从而达成以下目的:

  • 统一的 protobuf 使用界面
  • 无缝适配自己的 RPC 库、网络库等

目录文件介绍

│  main.go                               // main 函数,与 protoc 进程交互、 代码生成过程。写自己 protoc 插件时,通常会完全拷贝其内容
│
├─descriptor                             // 一个 proto 文件中的所有信息,通过 descriptor.proto 来定义
│      descriptor.pb.go
│      descriptor.proto
│
├─generator                              // proto 协议类生成过程。这个部分可以复用。generator 中定义了插件的方式,让你扩展自己的代码
│  │  generator.go
│  │  name_test.go
│  │
│  └─internal
│      |  工具类,略
│
├─grpc                                   // grpc service 定义过程,grpc 相关定义、函数。也是如何编写自己逻辑的参考例子
│      grpc.go
│
├─plugin                                 // 与 protoc 进程数据交互的数据格式定义
│      plugin.pb.go
│      plugin.pb.golden
│      plugin.proto
│
└─testdata
    │  测试文件,略

main.go

func main() {
	// Begin by allocating a generator. The request and response structures are stored there
	// so we can do error handling easily - the response structure contains the field to
	// report failure.
	g := generator.New()

	data, err := ioutil.ReadAll(os.Stdin)
	if err != nil {
		g.Error(err, "reading input")
	}

	if err := proto.Unmarshal(data, g.Request); err != nil {
		g.Error(err, "parsing input proto")
	}

	if len(g.Request.FileToGenerate) == 0 {
		g.Fail("no files to generate")
	}

	g.CommandLineParameters(g.Request.GetParameter())

	// Create a wrapped version of the Descriptors and EnumDescriptors that
	// point to the file that defines them.
	g.WrapTypes()

	g.SetPackageNames()
	g.BuildTypeNameMap()

	g.GenerateAllFiles()

	// Send back the results.
	data, err = proto.Marshal(g.Response)
	if err != nil {
		g.Error(err, "failed to marshal output proto")
	}
	_, err = os.Stdout.Write(data)
	if err != nil {
		g.Error(err, "failed to write output proto")
	}
}

通过 main.go 可以看出 protoc 与 其插件的工作过程:
hello.proto => protoc -> stdin -> protoc-gen-go --> CodeGeneratorRequest --> CodeGeneratorResponse -> stdout -> protoc => hello.pb.go

细节过程如下:

  • protoc 读入 hello.proto ,并把文件内容表达为 CodeGeneratorRequest 协议的 2 进制数据
  • protoc 把 2 进制数据 写入 stdin(管道相关知识)
  • protoc-gen-go 从 stdin 中读取 2 进制数据 (管道相关知识)
  • protoc-gen-go 解析 2 进制数据获得 CodeGeneratorRequest (内容是 descriptor.proto 中定义的元数据)
  • protoc-gen-go 根据 CodeGeneratorRequest ,在内存中生成 hello.pb.go 文件内容
  • protoc-gen-go 把生成的内容填充 CodeGeneratorResponse
  • protoc-gen-go 把 CodeGeneratorResponse 数据转化成 2 进程数据,写入 stdout (管道相关知识)
  • protoc 从 stdout 获取 2 进程数据 ,并解析得到 CodeGeneratorResponse
  • protoc 最后通过 CodeGeneratorResponse 获取输出文件内容,写入 hello.pb.go

因此,你写自己的go插件时,main.go 的代码基本上是原封不动的拷贝到自己的 main 函数中即可,作为启动运行流程

descriptor/descriptor.*

descriptor/descriptor.proto 、 descriptor/descriptor.pb.go 表示一个 proto 文件

这是 protobuf 官方规定维护的: protoc 与其插件数据互通的协议之一

这里有个有趣的鸡生蛋、蛋生鸡的现象:

  • protoc-gen-go 依赖 descriptor/descriptor.pb.go 代码实现、编译
  • descriptor/descriptor.pb.go 是有 protoc-gen-go 生成的

追溯根源的话,可以推测,早期 protoc-gen-go 应该是手动解析 descriptor.proto 内容。有了最初版本的 protoc-gen-go ,后面代码重构时才引入 descriptor/descriptor.proto 、 descriptor/descriptor.pb.go (这里仅自己猜测,请勿当真)

generator/generator.go

generator/generator.go 主要负责生成 golang 版本 protobuf 消息定义代码,并通过插件类,实现一些第 3 方的扩展代码

该文件各种细节,通常都不需要关注

重要的是以下 2 个方面:

  1. Plugin 插件接口定义,以及调用这些接口的上下文代码
  2. Generator 结构定义, 通过 Generator 对象可以获取 proto 定义文件中的任何东西(实际上了解即可,用到什么,看什么)
1. Plugin 插件接口定义
// A Plugin provides functionality to add to the output during Go code generation,
// such as to produce RPC stubs.
type Plugin interface {
	// Name identifies the plugin.
	Name() string
	// Init is called once after data structures are built but before
	// code generation begins.
	Init(g *Generator)
	// Generate produces the code generated by the plugin for this file,
	// except for the imports, by calling the generator's methods P, In, and Out.
	Generate(file *FileDescriptor)
	// GenerateImports produces the import declarations for this file.
	// It is called after Generate.
	GenerateImports(file *FileDescriptor)
}
  • Name 插件名字
  • Init 初始化函数,通常函数内需要保存下 g *Generator 对象,然后可以通过该对象获取所需要的
  • Generate 生成自己逻辑代码, file *FileDescriptor 通过该参数也可以获得 proto 文件的所有内容
  • GenerateImports 生成自己需要导入哪些头文件,通常不会在该函数中实现。还有更简单的方法(详细可看本文具体例子中的代码)
2. Generator 结构定义
// Generator is the type whose methods generate the output, stored in the associated response structure.
type Generator struct {
	*bytes.Buffer

	Request  *plugin.CodeGeneratorRequest  // The input.
	Response *plugin.CodeGeneratorResponse // The output.

	Param             map[string]string // Command-line parameters.
	PackageImportPath string            // Go import path of the package we're generating code for
	ImportPrefix      string            // String to prefix to imported package file names.
	ImportMap         map[string]string // Mapping from .proto file name to import path

	Pkg map[string]string // The names under which we import support packages

	outputImportPath GoImportPath                   // Package we're generating code for.
	allFiles         []*FileDescriptor              // All files in the tree
	allFilesByName   map[string]*FileDescriptor     // All files by filename.
	genFiles         []*FileDescriptor              // Those files we will generate output for.
	file             *FileDescriptor                // The file we are compiling now.
	packageNames     map[GoImportPath]GoPackageName // Imported package names in the current file.
	usedPackages     map[GoImportPath]bool          // Packages used in current file.
	usedPackageNames map[GoPackageName]bool         // Package names used in the current file.
	addedImports     map[GoImportPath]bool          // Additional imports to emit.
	typeNameToObject map[string]Object              // Key is a fully-qualified name in input syntax.
	init             []string                       // Lines to emit in the init function.
	indent           string
	pathType         pathType // How to generate output filenames.
	writeOutput      bool
	annotateCode     bool                                       // whether to store annotations
	annotations      []*descriptor.GeneratedCodeInfo_Annotation // annotations to store
}

作者基本上每个字段都写好了注释,不再一一复述

grpc/grpc.go

一个 grpc 代码扩展,通常你写自己的 go 插件,就可以拷贝这个文件,挖空,然后开工

引入包时,自动注册自己:

func init() {
	generator.RegisterPlugin(new(grpc))
}

输出生成的代码用:

// P forwards to g.gen.P.
func (g *micro) P(args ...interface{}) { g.gen.P(args...) }

其他的就是 Init 、 Generate 等实现,细节可以不关注

plugin/plugin.*

这是 protobuf 官方规定维护的: protoc 与其插件数据互通的协议之一

主要定义了:

  • CodeGeneratorRequest
    • protoc 给插件的数据
  • CodeGeneratorResponse
    • 插件给 protoc 的数据

除非你第一个写新语言插件,通常不需要关注

唯一需要注意的地方是命名,generator/generator.go 文件代码内也有 plugin !作者命名的不是很好

具体例子

本人在写 fananchong/v-micro ,参考 micro/go-micro ,与它的插件 protoc-gen-micro

在把 go-micro 的同步 Call 改成异步 Call 中时,需要调整 protoc-gen-micro 生成代码

于是写了 protoc-gen-vmicro , 并简化 protoc-gen-micro 代码

具体代码示例可参见:

https://github.com/fananchong/v-micro/tree/master/tools/protoc-gen-vmicro

目录结构
│  main.go                              // 基本完全拷贝 protoc-gen-go 的 main.go
│
├─examples                              // 生成例子,并调试用
│  └─greeter
│          g.bat
│          greeter.pb.go
│          greeter.proto
│          README.md
│
└─plugin
    └─micro
            micro.go                    // 自己代码扩展,实现上 import 引用 protoc-gen-go 的 generator.go 来达成本插件内容的调用
生成文件说明

比如 greeter.proto

syntax = "proto3";

package mypackage;

service Greeter {
	rpc Hello(Request) returns (Response) {}
}

message Request {
	string name = 1;
}

message Response {
	string msg = 1;
}

除了需要生成 go 版 protobuf 协议基本定义(protoc-gen-go 的 generator.go 的功能)

通过自己的 plugin/micro/micro.go ,达成输出类似以下代码:

// Client API for Greeter service

type GreeterService interface {
	Hello(ctx context.Context, req *Request, opts ...client.CallOption) error
}

type GreeterCallback interface {
	Hello(ctx context.Context, rsp *Response)
}

type greeterService struct {
	c    client.Client
	name string
}

func NewGreeterService(name string, hdcb GreeterCallback, c client.Client) GreeterService {
	if c == nil {
		panic("client is nil")
	}
	if len(name) == 0 {
		panic("name is nil")
	}
	if err := c.Handle(hdcb); err != nil {
		panic(err)
	}
	return &greeterService{
		c:    c,
		name: name,
	}
}

func (c *greeterService) Hello(ctx context.Context, req *Request, opts ...client.CallOption) error {
	r := c.c.NewRequest(c.name, "Greeter.Hello", req)
	err := c.c.Call(ctx, r, opts...)
	if err != nil {
		return err
	}
	return nil
}

// Server API for Greeter service

type GreeterHandler interface {
	Hello(ctx context.Context, req *Request, rsp *Response) error
}

func RegisterGreeterHandler(s server.Server, hdlr GreeterHandler) error {
	return s.Handle(hdlr)
}

以上

 类似资料: