go-zero 代碼生成器助你高效開發

Protocol Buffers 是谷歌推出的編碼標準,它在傳輸效率和編解碼性能上都要優於 JSON。但其代價則是需要依賴中間描述語言 (IDL) 來定義數據和服務的結構(通過 *.proto 文件),並且需要一整套的工具鏈(protoc 及其插件)來生成對應的序列化和反序列化代碼。除了谷歌官方提供的工具和插件(比如生成 go 代碼的 protoc-gen-go)外,開發者還可以開發或定製自己的插件,根據業務需要按照 proto 文件的定義生成代碼或者文檔。

goctl rpc 代碼生成工具開發的目的:

  1. proto 模版生成

  2. rpc server 代碼生成 → 得到的是 go-zero zrpc

  3. 內部包裝了 gRPC pb code 的生成

  4. 和 http server 一樣,提供了 go-zero 內置的一些管控中間件

我們可以注意到第 3 點,基本在不使用 codegen tool 情況下,開發者需要自己執行 protoc + protoc-gen-go 插件生成對應的 .pb.go 文件。整個過程比較繁瑣。

以上是 goctl rpc 的背景。本篇文章先從整體生成的角度闡述 goctl 生成過程,之後再分析一些關鍵的部分,從而讓各位開發者可以開發出契合自己業務系統的 codegen tool

整體結構

// 推薦使用 v3 版本。現在流行的 gRPC 框架也是使用 v3 版本。
syntax = "proto3";

// 每個 proto 文件需要定義自己的包名,類似 c++ 的名稱空間。
package hello;

// 數據結構通過 message 定義
message Echo {
  // 每個 message 可以有多個 field。
  // 每個 field 需要指定類型、字段名和編號。
  // Protocol Buffers 在內部使用編號區分字段,一旦指定就不能更改。
  string msg = 1;
}

// 服務使用 servcie 定義
service Demo {
  // 每個 service 可以定義多個 rpc
  // 每個 rpc 需要指定接口名、傳入消息和返回消息三部分。
  rpc Echo(Echo) returns (Echo);
}

所謂 代碼生成 其實也就是把 proto file(IDL) 的每一部分解析出來,然後再對應每一部分做模版渲染,生成對應的代碼即可。

而且在生成過程中,我們還可以藉助插件或者定製自己的插件。

我們先看看入口:

{
  Name:        "protoc",
  Usage:       "generate grpc code",
  UsageText:   "example: goctl rpc protoc xx.proto --go_out=./pb --go-grpc_out=./pb --zrpc_out=.",
  Description: "for details, see https://go-zero.dev/cn/goctl-rpc.html",
  Action:      rpc.ZRPC,
  Flags:       []cli.Flag{
   ...
  },
}

從 goctl.go(基本上 goctl 下面的命令入口都在這個文件可以找到)進入:

// ZRPC generates grpc code directly by protoc and generates
// zrpc code by goctl.
func ZRPC(c *cli.Context) error {
  ...

  grpcOutList := c.StringSlice("go-grpc_out")
  goOutList := c.StringSlice("go_out")
  zrpcOut := c.String("zrpc_out")
  style := c.String("style")
  home := c.String("home")
  remote := c.String("remote")
  branch := c.String("branch")
  ...
  goOut := goOutList[len(goOutList)-1]
  grpcOut := grpcOutList[len(grpcOutList)-1]
  ...
 
  var ctx generator.ZRpcContext
  ...
  // 將args中的值逐個賦值給 ZRpcContext,作爲env context注入 generator
  g, err := generator.NewDefaultRPCGenerator(style, generator.WithZRpcContext(&ctx))
  if err != nil {
   return err
  }

  return g.Generate(source, zrpcOut, nil)
}

g.Generate(source, zrpcOut, nil) → goctl rpc 生成的核心函數,負責了整個生命週期:

  1. 解析 → proto parse

  2. 模版填充 → proto item into template

  3. 文件生成 → touch generate file

generator

func (g *RPCGenerator) Generate(src, target string, protoImportPath []string, goOptions ...string) error {
  ...
  // proto parser
  p := parser.NewDefaultProtoParser()
  proto, err := p.Parse(src)
  
  dirCtx, err := mkdir(projectCtx, proto, g.cfg, g.ctx)
  
  // generate Go code
  err = g.g.GenEtc(dirCtx, proto, g.cfg)
  err = g.g.GenPb(dirCtx, protoImportPath, proto, g.cfg, g.ctx, goOptions...)
  err = g.g.GenConfig(dirCtx, proto, g.cfg)
  err = g.g.GenSvc(dirCtx, proto, g.cfg)
  err = g.g.GenLogic(dirCtx, proto, g.cfg)
  err = g.g.GenServer(dirCtx, proto, g.cfg)
  err = g.g.GenMain(dirCtx, proto, g.cfg)
  err = g.g.GenCall(dirCtx, proto, g.cfg)
  ...
}

上圖展示 Generate() 的代碼生成過程。


這裏提前說明一些 GenPb() 的過程。爲什麼要說這個呢?goctl 是脫離 protoc 的工具體系,包括和 protoc 插件機制,所以要生成 .pb.go 文件,之間是怎麼耦合的呢?

首先查詢是否有內置的 xxx 插件,如果沒有內置的 xxx 插件那麼將繼續查詢當前系統中是否存在 protoc-gen-xxx 命名的可執行程序,最終通過查詢到的插件生成代碼。

go-zero 是沒有對 protoc 額外編寫插件輔助生成代碼。所以默認使用的就是 protoc-gen-xxx 生成的 go 代碼。

func (g *DefaultGenerator) GenPb(ctx DirContext, 
  protoImportPath []string, 
  proto parser.Proto, 
  _ *conf.Config, 
  c *ZRpcContext, 
  goOptions ...string) error {
 ...
 // protoc 命令string
 cw := new(bytes.Buffer)
 ...
 // cw.WriteString("protoc ")
 // cw.WriteString(some command shell)
 command := cw.String()
 g.log.Debug(command)
 _, err := execx.Run(command, "")
 if err != nil {
  if strings.Contains(err.Error(), googleProtocGenGoErr) {
   return errors.New(`unsupported plugin protoc-gen-go which installed from the following source:
google.golang.org/protobuf/cmd/protoc-gen-go, 
github.com/protocolbuffers/protobuf-go/cmd/protoc-gen-go;

Please replace it by the following command, we recommend to use version before v1.3.5:
go get -u github.com/golang/protobuf/protoc-gen-go`)
  }

  return err
 }
 return nil
}

一句話描述 GenPb() :

根據前面 proto parse 解析出來的結構和路徑拼裝 protoc 編譯運行的命令,然後 execx.Run(command, "") 直接執行這條命令即可。

所以如果開發者需要加入自己的插件,可以自行修改其中 cw.WriteString(some command shell) 寫入自己的執行命令邏輯即可。

總結

以上就是本文的全部內容了。本文從 goctl rpc 生成 rpc 代碼的入口分析了整個生成流程,其中特意提到 .pb.go 文件的生成,開發者可以從此代碼部分切入 goctl rpc,加入自己編寫的 proto 插件。當然還有其他部分,會在後續的文章繼續分析。

本系列文章的目的:順便帶大家改造一個屬於自己的 rpc codegen tool。

項目地址

https://github.com/zeromicro/go-zero

歡迎使用 go-zerostar 支持我們!

微信交流羣

關注『微服務實踐』公衆號並點擊 交流羣 獲取社區羣二維碼。

微服務實踐 分享微服務的原理和最佳實踐,講透服務治理的底層原理,帶你細讀 go-zero 源碼。go-zero 是一個集成了各種工程實踐的 web 和 rpc 框架,旨在縮短從需求到上線的距離。公衆號文章勘誤在知乎號:萬俊峯 Kevin

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/i4MqbHYzja5Tvzptjt9chQ