1. 前言
大家好,这是我解读 kitex 源码的第二篇文章,是继续上回未分析的部分而写的,希望大家可以阅读一下,这次我使用的是和上次相反的思路来分析代码的。
对上一篇文章有兴趣的朋友可以看这里,上一篇文章主要介绍了 Kitex tool 大致生成文件的思路,但是我并没有讲述它是如何从template 变成 go 文件的过程,而是将它忽略了。
上述未补充的思路将由这篇文章进行补充。
这次文章需要了解 golang 中的 template (tpl),希望大家看之前先去了解一下它的用法。我会带大家一点破面,慢慢的来解析这些代码
2. 介绍
CloudWeGo-Kitex
Kitex[kaɪt’eks] 为字节跳动内部的 Golang 微服务 RPC 框架,具有高性能、强可扩展的特点,在字节内部已广泛使用。如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。
3. 相反的开始,从”终点“开始
克隆 Kitex 的代码,
git clone https://github.com/cloudwego/kitex.git
打开并观察一下kitex tool 的具体目录结构,
我们可以发现, 模板文件中的绝大部分就储存在其中的 internal/pkg/tpl 目录之中
root@root:/kitex/tool$ tree
.
├── cmd
│ └── kitex
│ ├── args.go
│ └── main.go
└── internal
└── pkg
├── generator
│ ├── completor.go
│ ├── feature.go
│ ├── generator.go
│ ├── generator_test.go
│ └── type.go
├── log
│ └── log.go
├── pluginmode
│ ├── protoc
│ │ ├── plugin.go
│ │ ├── protoc.go
│ │ └── util.go
│ └── thriftgo
│ ├── convertor.go
│ ├── file_tpl.go
│ ├── patcher.go
│ ├── plugin.go
│ └── struct_tpl.go
├── tpl
│ ├── bootstrap_tpl.go
│ ├── build_tpl.go
│ ├── client_tpl.go
│ ├── handler_tpl.go
│ ├── invoker_tpl.go
│ ├── main_tpl.go
│ ├── server_tpl.go
│ └── service_tpl.go
└── util
└── util.go
于是我们取出一个较为简单 一个模板文件作为示例:
eg: main_tpl.go
package tpl
// MainTpl is the template for generating main.go.
var MainTpl = `
package main
import (
{{- range $path, $alias := .Imports}}
{{$alias }}"{{$path}}"
{{- end}}
)
func main() {
svr := {{.PkgRefName}}.NewServer(new({{.ServiceName}}Impl))
err := svr.Run()
if err != nil {
log.Println(err.Error())
}
}
`
这是 官方示例 kitex-examples hello 文件夹中的 main.go
package main
import (
"log"
api "github.com/cloudwego/kitex-examples/hello/kitex_gen/api/hello"
)
func main() {
svr := api.NewServer(new(HelloImpl))
err := svr.Run()
if err != nil {
log.Println(err.Error())
}
}
有没有发现这两个文件的结构基本一致?只是在占位符 {{ }} 中的变量被替换成为了具体的变量,因此我们可以从这些变量出发,来看看它们的作用。
在 IDE 中搜索 PkgRefName, 结果发现它存在于 tool/internal/pkg/generator/type.go 中
从以下以下内容可知,此为 渲染 (render) 模板的文件,那是谁调用的它呢,我们可以继续往上找
package generator
import (
"bytes"
"path/filepath"
"strings"
"text/template"
"github.com/cloudwego/kitex/tool/internal/pkg/util"
)
// File .
type File struct {
Name string
Content string
}
// PackageInfo contains information to generate a package for a service.
type PackageInfo struct {
Namespace string // a dot-separated string for generating service package under kitex_gen
Dependencies map[string]string // package name => import path, used for searching imports
*ServiceInfo // the target service
// the following fields will be filled and used by the generator
Codec string
Version string
RealServiceName string
Imports map[string]string // import path => alias
ExternalKitexGen string
Features []feature
}
// AddImport .
func (p *PackageInfo) AddImport(pkg, path string) {
if pkg != "" {
if p.ExternalKitexGen != "" && strings.Contains(path, KitexGenPath) {
parts := strings.Split(path, KitexGenPath)
path = filepath.Join(p.ExternalKitexGen, parts[len(parts)-1])
}
if strings.HasSuffix(path, "/"+pkg) || path == pkg {
p.Imports[path] = ""
} else {
p.Imports[path] = pkg
}
}
}
// AddImports .
func (p *PackageInfo) AddImports(pkgs ...string) {
for _, pkg := range pkgs {
if path, ok := p.Dependencies[pkg]; ok {
p.AddImport(pkg, path)
} else {
p.AddImport(pkg, pkg)
}
}
}
// PkgInfo .
type PkgInfo struct {
PkgName string
PkgRefName string
ImportPath string
}
// ServiceInfo .
type ServiceInfo struct {
PkgInfo
ServiceName string
RawServiceName string
ServiceTypeName func() string
Base *ServiceInfo
Methods []*MethodInfo
CombineServices []*ServiceInfo
HasStreaming bool
}
// AllMethods returns all methods that the service have.
func (s *ServiceInfo) AllMethods() (ms []*MethodInfo) {
ms = s.Methods
for base := s.Base; base != nil; base = base.Base {
ms = append(base.Methods, ms...)
}
return ms
}
// MethodInfo .
type MethodInfo struct {
PkgInfo
ServiceName string
Name string
RawName string
Oneway bool
Void bool
Args []*Parameter
Resp *Parameter
Exceptions []*Parameter
ArgStructName string
ResStructName string
IsResponseNeedRedirect bool // int -> int*
GenArgResultStruct bool
ClientStreaming bool
ServerStreaming bool
}
// Parameter .
type Parameter struct {
Deps []PkgInfo
Name string
RawName string
Type string // *PkgA.StructB
}
var funcs = map[string]interface{}{
"ToLower": strings.ToLower,
"LowerFirst": util.LowerFirst,
"UpperFirst": util.UpperFirst,
"NotPtr": util.NotPtr,
"HasFeature": HasFeature,
}
// Task .
type Task struct {
Name string
Path string
Text string
*template.Template
}
// Build .
func (t *Task) Build() error {
x, err := template.New(t.Name).Funcs(funcs).Parse(t.Text)
if err != nil {
return err
}
t.Template = x
return nil
}
// Render .
func (t *Task) Render(data interface{}) (*File, error) {
if t.Template == nil {
err := t.Build()
if err != nil {
return nil, err
}
}
var buf bytes.Buffer
err := t.ExecuteTemplate(&buf, t.Name, data)
if err != nil {
return nil, err
}
return &File{t.Path, buf.String()}, nil
}
4. 进入 generator.go 自底向上
从上面的分析中,我猜测 Render 被调用的可能性较高,于是我们深入进去可以到达另一个文件: tool/internal/pkg/generator.go
package generator
import (
...
)
// Constants .
// 这些是 kitex 生成过后的基本文件名
const (
KitexGenPath = "kitex_gen"
KitexImportPath = "github.com/cloudwego/kitex"
DefaultCodec = "thrift"
BuildFileName = "build.sh"
BootstrapFileName = "bootstrap.sh"
HandlerFileName = "handler.go"
MainFileName = "main.go"
ClientFileName = "client.go"
ServerFileName = "server.go"
InvokerFileName = "invoker.go"
ServiceFileName = "*service.go"
)
...
// Generator generates the codes of main package and scripts for building a server based on kitex.
// 这个接口定义了 生成 service 和 package main 的函数,我们看 GenerateMainPackage 因为我们是从 main_tpl 上来的嘛
type Generator interface {
GenerateService(pkg *PackageInfo) ([]*File, error)
GenerateMainPackage(pkg *PackageInfo) ([]*File, error)
}
...
func (g *generator) GenerateMainPackage(pkg *PackageInfo) (fs []*File, err error) {
// 设置 PakageInfo 的信息
g.updatePackageInfo(pkg)
// 设置了 generate 的任务
tasks := []*Task{
{
// 生成的变量名: build.sh 为上方定义的 const
Name: BuildFileName,
// 输出的具体路径 eg: hello/build.sh
Path: filepath.Join(g.OutputPath, BuildFileName),
// 用于 render 的 template
Text: tpl.BuildTpl,
},
{
Name: BootstrapFileName,
Path: filepath.Join(g.OutputPath, "script", BootstrapFileName),
Text: tpl.BootstrapTpl,
},
}
// generate main.go with invoker when main package generate
// 这是 GenerateInvoker 的注释 查看是否生成了main.go
// 未生成就 add 进任务中
if !g.Config.GenerateInvoker {
tasks = append(tasks, &Task{
Name: MainFileName,
Path: filepath.Join(g.OutputPath, MainFileName),
Text: tpl.MainTpl,
})
}
// 按序执行任务
for _, t := range tasks {
// 生成过就 skipped
if util.Exists(t.Path) {
log.Info(t.Path, "exists. Skipped.")
continue
}
g.setImports(t.Name, pkg)
handle := func(task *Task, pkg *PackageInfo) (*File, error) {
// 注意, 这里就是点进来的入口
return task.Render(pkg)
}
f, err := g.chainMWs(handle)(t, pkg)
if err != nil {
return nil, err
}
fs = append(fs, f)
}
handlerFilePath := filepath.Join(g.OutputPath, HandlerFileName)
if util.Exists(handlerFilePath) {
comp := newCompleter(
pkg.ServiceInfo.AllMethods(),
handlerFilePath,
pkg.ServiceInfo.ServiceName)
f, err := comp.CompleteMethods()
if err != nil {
if err == errNoNewMethod {
return fs, nil
}
return nil, err
}
fs = append(fs, f)
} else {
task := Task{
Name: HandlerFileName,
Path: handlerFilePath,
Text: tpl.HandlerTpl,
}
g.setImports(task.Name, pkg)
handle := func(task *Task, pkg *PackageInfo) (*File, error) {
return task.Render(pkg)
}
f, err := g.chainMWs(handle)(&task, pkg)
if err != nil {
return nil, err
}
fs = append(fs, f)
}
return
}
这里只是举个例,我们可以发现在这里 render 函数被 GenerateMainPackage 调用了, 在这个环节, main.go, build.sh等模板将会被渲染完成, 自此我们可以通过这个函数再次向上探索.
5. 精妙的文件设计
这时候我们会发现它在一个文件中使用了两次,该文件为 plugin.go
├── pluginmode
│ ├── protoc
│ │ ├── plugin.go
│ │ ├── protoc.go
│ │ └── util.go
│ └── thriftgo
│ ├── convertor.go
│ ├── file_tpl.go
│ ├── patcher.go
│ ├── plugin.go
│ └── struct_tpl.go
我以 protoc 为例,主要原因是对于我来说,使用 grpc 是较多的。我们会发现文件夹的设计很有意思,它让我感到眼熟
├── pluginmode
├── protoc
└── thriftgo
没错,如果你看过我的上篇文章,你会发现它的设计对应着一段代码, 一段 tool/cmd/kitex/main.go 中的代码
package main
import (
...
)
...
func main() {
// run as a plugin
switch filepath.Base(os.Args[0]) {
case thriftgo.PluginName:
os.Exit(thriftgo.Run())
case protoc.PluginName:
os.Exit(protoc.Run())
}
}
这表明 代码的生成就是在这个 switch 中进行的,我们进入 protoc.Run() 继续观察, 很明显它执行了私有的 run()
忽略一些代码我们会看到这段注释, // generate files , 很明显在它下面就是代码就会生成
package protoc
func run(opts protogen.Options) error {
...
// init plugin
pp := new(protocPlugin)
// generate files
gen, err := opts.New(req)
if err != nil {
return err
}
pp.process(gen)
gen.SupportedFeatures = gengo.SupportedFeatures
// construct plugin response
resp := gen.Response()
out, err := proto.Marshal(resp)
if err != nil {
return err
}
if _, err := os.Stdout.Write(out); err != nil {
return err
}
return nil
}
于是我们挨个试探以下,于是我们会发现 pp.process(gen) 中有着一条熟悉的代码, GenerateMainPackage()
func (pp *protocPlugin) process(gen *protogen.Plugin) {
...
fs, err := pp.kg.GenerateMainPackage(&pp.PackageInfo)
...
return
}
至此,我们可以说简约的理顺了代码生成的原理执行顺序为
tool/cmd/main.go (选择生成模式)
|
|
tool/internal/pkg/pluginmode/protoc/protoc.go (生成grpc相关代码, 并将参数向下传递)
|
|
tool/internal/pkg/generator/generator.go (将参数收集为 []Task, 并结合 template 输出具体代码)
6. 总结
文章的主体部分在此到一段落,那我们从文章中学到什么了呢 ? 这是需要思考总结的:
- 见识到了 企业中模板文件的使用方式和定义规范
- 见到了 好的代码就是最好的注释的 诠释案例
- 既然我们知道了生成的原理,我们自己能不能为了自己项目的需求,添加或者修改业务逻辑 以达到个性化的目的呢?
以上是我本人对于这次源码阅读的一些思考,希望可以对阅读本文的朋友带来一定的帮助,感谢大家耐心看完!
\