對於 CloudWeGo kitex 生成工具的原始碼分析

語言: CN / TW / HK

theme: juejin

大家好,前兩天我在網上怎麼也搜尋也搜不到 關於 Kitex 的解析文章,基本只是介紹 bytedance 出了個 kitex 框架之類的一模一樣的無效資訊,我感覺很難受

為什麼發在掘金呢,因為這是我在 google 的時候有時會出現在我頁面的有用網站,baidu 實在是不行。

以下內容為我對於 kitex 中 程式碼生成檔案的解析說明

Kitex 文件官網

1. 我認為在解析原始碼的時候最好遵循以下幾個原則

  1. 要有紮實的語言基礎知識
  2. 熟練的使用搜素引擎, baidu 不行!
  3. 遵循由淺入深由表及裡的原則, 不要一口吃個大胖子,直接失去學習的興趣
  4. 擁有較為完善的英語水平,因為大多開源專案都是面向國際的,所以一般選用英文作為註釋,看不懂這是我們的問題,肯定不是開發人員的問題啊

2. 開始分析 main.go

由文件提示可知,kitex 工具檔案是在專案的 github.com/cloudwego/kitex/tool/cmd 目錄中

image-20220520224653636.png

.  └── kitex   ├── args.go   └── main.go

  • main.go 完成命令列的執行邏輯
  • args.go 主要用於解析命令列引數

下面從 main.go 開始分析, 以下是主要邏輯

// 新增 version 引數  func init() {   ...  }  // 執行主體 ...  func main() {   ...  }  // 指定 IDL 檔案的generator tool path  func lookupTool(idlType string) string {   ...  }  // 形成 執行kitex 生成程式碼的命令  func buildCmd(a *arguments, out io.Writer) *exec.Cmd {   ...  }

然後我們從 func main() 進行分析, 以下為基本邏輯

func main() {   // run as a plugin   // 決定使用哪種 外掛   switch filepath.Base(os.Args[0]) {   // thrift-gen-kitex   case thriftgo.PluginName:   os.Exit(thriftgo.Run())   // protoc-gen-kitex   case protoc.PluginName:   os.Exit(protoc.Run())   }     //TODO: 分析 命令列引數   args.parseArgs()     out := new(bytes.Buffer)   // 返回了生成了的例如 protoc-gen-kitex 的可執行檔案cmd   cmd := buildCmd(&args, out)   // run cmd   err := cmd.Run()   if err != nil {   if args.Use != "" {   out := strings.TrimSpace(out.String())   if strings.HasSuffix(out, thriftgo.TheUseOptionMessage) {   os.Exit(0)   }   }   os.Exit(1)   }  }

再然後我們進入 args.parseArgs() 中分析

func (a *arguments) parseArgs() {   // 設定flags   f := a.buildFlags()   // 分析 flag   if err := f.Parse(os.Args[1:]); err != nil {   log.Warn(os.Stderr, err)   os.Exit(2)   }   // 將引數賦值給配置   log.Verbose = a.Verbose   // 檢查 從外新增的引數   for _, e := range a.extends {   e.check(a)   }   // 檢查...   a.checkIDL(f.Args())   a.checkServiceName()   a.checkPath()  }

我們可以發現 kitex/tool/cmd/kitex/args.go 中的 buildFlag(),使用了golang/src/flag 庫,這是由 golang 官方支援實現命令列的庫,以上程式碼使用命令列中的第一個引數作為一個 flag,第二個引數為flag使用出現 error的處理方法

func (a *arguments) buildFlags() *flag.FlagSet {   f := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)   ...  }

函式中類似的方法較多, 我們只舉例一個

func (f FlagSet) BoolVar(p bool, name string, value bool, usage string)

它實了現引數的繫結

func (a *arguments) buildFlags() *flag.FlagSet {   f := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)   // 設定子命令   f.BoolVar(&a.NoFastAPI, "no-fast-api", false,   "Generate codes without injecting fast method.")   ...  }  ​  type arguments struct {   generator.Config   // 額外新增的 flag   extends []*extraFlag  }

被繫結的引數

package generator  ​  type Config struct {   Verbose bool   GenerateMain bool // whether stuff in the main package should be generated   GenerateInvoker bool // generate main.go with invoker when main package generate   Version string   NoFastAPI bool   ModuleName string   ServiceName string   Use string   IDLType string   Includes util.StringSlice   ThriftOptions util.StringSlice   ProtobufOptions util.StringSlice   IDL string // the IDL file passed on the command line   OutputPath string // the output path for main pkg and kitex_gen   PackagePrefix string   CombineService bool // combine services to one service   CopyIDL bool   ThriftPlugins util.StringSlice   Features []feature  }

然後再從 kitex 中的程式碼生成工具命令入手

這是官方文件中的示例

kitex -module "your_module_name" -service a.b.c hello.thrift

其中 hello.thrift 引數由於沒有形成鍵值對,所以屬於 non-flag , 由 buildFlags 中的 a.checkIDL(f.Args()) 進行讀取

func (a *arguments) buildFlags() *flag.FlagSet {   f := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)   ...   // 檢查使用哪種 IDL 語言   a.CheckIDL(f.Args())  }

我們再深入看看 f.Args 的原始碼, 從註釋知曉 Args() 讀取的為 non-flag 的引數,由此通過 CheckIDL() 便可以判斷使用了哪種 IDL 語言

package flag  ​  // Args returns the non-flag arguments.  func (f *FlagSet) Args() []string { return f.args }

3. -module 為什麼有時候可以可有可無 ?

官網中還有一個有意思的說明, 當前目錄是在 $GOPATH/src 下的一個目錄,那麼可以不指定 -module,這部分的邏輯在 args.go 中的 checkPath() 方法中

image-20220520232852175.png

func (a *arguments) checkPath() {   // go 的路徑   pathToGo, err := exec.LookPath("go")   ...   // 獲取 gopath/src   gosrc := filepath.Join(util.GetGOPATH(), "src")   gosrc, err = filepath.Abs(gosrc)   ...   curpath, err := filepath.Abs(".")   // 是不是存在gopath/src 中   if strings.HasPrefix(curpath, gosrc) {   if a.PackagePrefix, err = filepath.Rel(gosrc, curpath); err != nil {   log.Warn("Get GOPATH/src relpath failed:", err.Error())   os.Exit(1)   }   a.PackagePrefix = filepath.Join(a.PackagePrefix, generator.KitexGenPath)   } else {   if a.ModuleName == "" {   log.Warn("Outside of $GOPATH. Please specify a module name with the '-module' flag.")   os.Exit(1)   }   }   // 重點   if a.ModuleName != "" {   module, path, ok := util.SearchGoMod(curpath)   if ok {   // go.mod exists   if module != a.ModuleName {   log.Warnf("The module name given by the '-module' option ('%s') is not consist with the name defined in go.mod ('%s' from %s)\n",   a.ModuleName, module, path)   os.Exit(1)   }   if a.PackagePrefix, err = filepath.Rel(path, curpath); err != nil {   log.Warn("Get package prefix failed:", err.Error())   os.Exit(1)   }   a.PackagePrefix = filepath.Join(a.ModuleName, a.PackagePrefix, generator.KitexGenPath)   } else {   if err = initGoMod(pathToGo, a.ModuleName); err != nil {   log.Warn("Init go mod failed:", err.Error())   os.Exit(1)   }   a.PackagePrefix = filepath.Join(a.ModuleName, generator.KitexGenPath)   }   }  ​   if a.Use != "" {   a.PackagePrefix = a.Use   }   a.OutputPath = curpath  }

從以上程式碼為什麼 GOPATH/src 中可以不使用 -module, 因為 $GOPATH/src 中是有go.mod 目錄的,所以 -module 其實基本是屬於必須的引數,如果沒有看到src目錄,大家可以自行搜尋一下原因,通過自己的思考得到答案是很有意思的.

4. 繼續分析main.go

看完了上面的分析我們再轉回 main.go, 從 init() 可知該函式添加了version 引數, 我感覺個人可以通過此對kitex 進行侵入性小的個人定製

func init() {   var queryVersion bool   args.addExtraFlag(&extraFlag{   apply: func(f *flag.FlagSet) {   f.BoolVar(&queryVersion, "version", false,   "Show the version of kitex")   },   check: func(a *arguments) {   if queryVersion {   println(a.Version)   os.Exit(0)   }   },   })  }

從下可知 buildCmd 是一個重要的方法,我們下來開始解析

func main() {   ...   out := new(bytes.Buffer)   // 返回了生成了的例如 protoc-gen-kitex 的可執行檔案cmd   cmd := buildCmd(&args, out)   // run cmd   err := cmd.Run()   if err != nil {   if args.Use != "" {   out := strings.TrimSpace(out.String())   if strings.HasSuffix(out, thriftgo.TheUseOptionMessage) {   os.Exit(0)   }   }   os.Exit(1)   }  }

從程式碼可知該函式 使用了exec.Cmd{} 這個 golang 原生方法,這個我覺得大家可以自己點進原始碼看看, 學習的時候畢竟是要思考的嘛

func buildCmd(a *arguments, out io.Writer) *exec.Cmd {   // Pack 的作用是將配置資訊解析成key=value的格式   // eg: IDL=thrift,Version=1.2   kas := strings.Join(a.Config.Pack(), ",")   cmd := &exec.Cmd{   // 指定 IDL 檔案的generator tool path   Path: lookupTool(a.IDLType),   Stdin: os.Stdin,   Stdout: &teeWriter{out, os.Stdout},   Stderr: &teeWriter{out, os.Stderr},   }     if a.IDLType == "thrift" {   cmd.Args = append(cmd.Args, "thriftgo")   for _, inc := range a.Includes {   cmd.Args = append(cmd.Args, "-i", inc)   }   a.ThriftOptions = append(a.ThriftOptions, "package_prefix="+a.PackagePrefix)   gas := "go:" + strings.Join(a.ThriftOptions, ",")   if a.Verbose {   cmd.Args = append(cmd.Args, "-v")   }   if a.Use == "" {   cmd.Args = append(cmd.Args, "-r")   }   cmd.Args = append(cmd.Args,   // generator.KitexGenPath = kitex_gen   "-o", generator.KitexGenPath,   "-g", gas,   "-p", "kitex:"+kas,   )   for _, p := range a.ThriftPlugins {   cmd.Args = append(cmd.Args, "-p", p)   }   cmd.Args = append(cmd.Args, a.IDL)   } else {   a.ThriftOptions = a.ThriftOptions[:0]   // "protobuf"   cmd.Args = append(cmd.Args, "protoc")   for _, inc := range a.Includes {   cmd.Args = append(cmd.Args, "-I", inc)   }   outPath := filepath.Join(".", generator.KitexGenPath)   if a.Use == "" {   os.MkdirAll(outPath, 0o755)   } else {   outPath = "."   }   cmd.Args = append(cmd.Args,   "--kitex_out="+outPath,   "--kitex_opt="+kas,   a.IDL,   )   }   log.Info(strings.ReplaceAll(strings.Join(cmd.Args, " "), kas, fmt.Sprintf("%q", kas)))   return cmd  }

這是我大致分析lookupTook 方法的註釋

func lookupTool(idlType string) string {   // 返回此程序可執行路徑名   exe, err := os.Executable()   if err != nil {   log.Warn("Failed to detect current executable:", err.Error())   os.Exit(1)   }     // 找出可執行檔名 eg: kitex   dir := filepath.Dir(exe)   // 拼接path eg: kitex protoc-gen-kitex   pgk := filepath.Join(dir, protoc.PluginName)   tgk := filepath.Join(dir, thriftgo.PluginName)     link(exe, pgk)   link(exe, tgk)     tool := "thriftgo"   if idlType == "protobuf" {   tool = "protoc"   }   // 尋找 PATH 中的指定可執行檔案   // e.g: /usr/local/bin/protoc-gen-kitex   path, err := exec.LookPath(tool)   if err != nil {   log.Warnf("Failed to find %q from $PATH: %s. Try $GOPATH/bin/%s instead\n", path, err.Error(), tool)   path = filepath.Join(util.GetGOPATH(), "bin", tool)   }   return path  }

由此 只要在 kitex 此目錄執行 go build 命令,再放入 GOPATH 下,kitex 的執行檔案就生效了

5. 總結

我這次解析原始碼主要是因為 cloudwego 開源不久,我乍看之下只推出了 kitex RPC 框架 和 Netpoll 網路庫, 網路上好像也沒有什麼解析,看到位元組跳動的 CSG 所以抽空寫了一下,希望對樂意學習的同學有幫助。

由於我本人也只是接觸 golang 不到一個月,並且寫的匆忙,所以難免有些紕漏,望原諒