大家好,前两天我在网上怎么也查找也搜不到 关于 Kitex 的解析文章,根本仅仅介绍 bytedance 出了个 kitex 框架之类的一模一样的无效信息,我感觉很难过

为什么发在呢,因为这是我在 google 的时分有时会出现在我页面的有用网站,baidu 实在是不可。

以下内容为我关于 kitex 中 代码生成文件的解析阐明

Kitex 文档官网

1. 我认为在解析源码的时分最好遵从以下几个原则

  1. 要有厚实的言语基础知识
  2. 熟练的运用搜素引擎, baidu 不可!
  3. 遵从由浅入深由外至内的原则, 不要一口吃个大胖子,直接失掉学习的爱好
  4. 具有较为完善的英语水平,因为大多开源项目都是面向世界的,所以一般选用英文作为注释,看不懂这是咱们的问题,必定不是开发人员的问题啊

2. 开端剖析 main.go

由文档提示可知,kitex 工具文件是在项目的 github.com/cloudwego/kitex/tool/cmd 目录中

对于 CloudWeGo kitex 生成工具的源码分析

.
└── 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() 办法中

对于 CloudWeGo kitex 生成工具的源码分析

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 不到一个月,并且写的匆忙,所以难免有些疏忽,望宽恕