对于 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 不到一个月,并且写的匆忙,所以难免有些纰漏,望原谅