Protocol Buffers
是谷歌推出的編碼標準,它在傳輸效率和編解碼效能上都要優於 JSON。但其代價則是需要依賴中間描述語言(IDL)來定義資料和服務的結構(通過 *.proto
檔案),並且需要一整套的工具鏈(protoc 及其外掛)來生成對應的序列化和反序列化程式碼。除了谷歌官方提供的工具和外掛(比如生成 go 程式碼的 protoc-gen-go)外,開發者還可以開發或定製自己的外掛,根據業務需要按照 proto 檔案的定義生成程式碼或者文件。
而 goctl rpc
程式碼生成工具開發的目的:
- proto 模版生成
rpc server
程式碼生成 → 得到的是go-zero zrpc
- 內部包裝了
gRPC pb code
的生成 - 和 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 生成的核心函式,負責了整個生命週期:
- 解析 → proto parse
- 模版填充 → proto item into template
- 檔案生成 → 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-zero
並 star 支援我們!
微信交流群
關注『微服務實踐』公眾號並點選 交流群 獲取社群群二維碼。