大家好,前两天我在网上怎么也查找也搜不到 关于 Kitex 的解析文章,根本仅仅介绍 bytedance 出了个 kitex 框架之类的一模一样的无效信息,我感觉很难过
为什么发在呢,因为这是我在 google 的时分有时会出现在我页面的有用网站,baidu 实在是不可。
以下内容为我关于 kitex 中 代码生成文件的解析阐明
Kitex 文档官网
1. 我认为在解析源码的时分最好遵从以下几个原则
- 要有厚实的言语基础知识
- 熟练的运用搜素引擎, baidu 不可!
- 遵从由浅入深,由外至内的原则, 不要一口吃个大胖子,直接失掉学习的爱好
- 具有较为完善的英语水平,因为大多开源项目都是面向世界的,所以一般选用英文作为注释,看不懂这是咱们的问题,必定不是开发人员的问题啊
2. 开端剖析 main.go
由文档提示可知,kitex 工具文件是在项目的 github.com/cloudwego/kitex/tool/cmd 目录中
.
└── 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() 办法中
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 不到一个月,并且写的匆忙,所以难免有些疏忽,望宽恕