gengo 代码生成核心实现#
Kubernetes 的代码生成器都是在 k8s.io/gengo 包的基础上实现的。我们在前面章节中介绍 deepcopy-gen、defaulter-gen、conversion-gen、openapi-gen、prerelease-lifecycle-gen、client-gen、lister-gen、informer-gen、applyconfiguration-gen 等代码生成器的用法。代码生成器都会通过一个输入包路径 (--input-dirs) 参数,根据 gengo 的词法分析、生成抽象语法树 (Abstract Syntax Tree, AST) 等操作,最终生成并输出 (--output-file-base) 代码。gengo 代码目录结构如下。
$ tree vendor/k8s.io/gengo/ -L 1
vendor/k8s.io/gengo/
├── args
├── examples
├── generator
├── namer
├── parser
└── typesgengo 代码目录结构说明如表 13-7 所示。
表 13-7 gengo 代码目录结构说明
| 目录 | 说明 |
|---|---|
args | 代码生成器的通用 flag 参数 |
examples | gengo 提供的官方示例代码 |
generator | 代码生成器通用接口 Generator |
namer | 命名管理,支持创建不同类型的命名。例如,根据类型生成名称,代码定义 type foo string,能够生成 func FooPrinter(f *foo) { Print(string(*f)) } |
parser | 代码解析器,用来构造抽象语法树 |
types | 定义了 gengo 中的核心数据结构 |
代码生成与编译器原理#
gengo 的代码生成逻辑与编译器原理非常类似,大致可分为以下几个过程,gengo 生成代码的过程如下:

- Gather The Info:收集 Go 语言源码文件信息及内容。
- Lexer/Parser:通过 Lexer 词法分析器进行一系列词法分析。
- AST Generator:生成抽象语法树。
- Type Checker:对抽象语法树进行类型检查。
- Code Generation:生成代码,将抽象语法树转换为机器代码。
收集Go 包信息#
Go 语言没有预处理器、宏定义或 #define 声明来控制指定平台。相反,Go 语言标准库提供 go/build 工具,该工具支持通过 Go 语言的 build tag 机制来构建约束条件 (Build Constraints),我们在阅读 Kubernetes 源码时会经常看到与 // +build linux,darwin 类似的包注释信息。这就是 Go 语言编译时的约束条件,也被称为条件编译。
Go 语言的条件编译有两种方法定义,分别介绍如下。
- 构建标签 (build tag):在源码里添加注释信息,如
// +build linux,该标签决定了这个源码文件只在 Linux 平台下才会被编译。 - 文件后缀:改变 Go 语言代码文件名的后缀,如
foo_linux.go,该后缀决定了这个源码文件只在 Linux 平台下才会被编译。
另外,go/build 包有几个重要的类型和方法。Context 类型指定构建上下文环境,如 GOARCH、GOOS、GOROOT、GOPATH 等。Package 类型用于描述 Go 包信息。Import 方法用于导入指定的包,返回该包的 Package 指针类型,收集 Go 包的信息。它们用于处理 Go 项目的目录结构、源码、语法、基本操作等。
gengo 收集 Go 包信息可分为两步。第一步,为生成的代码文件设置构建标签;第二步,收集 Go 包信息并读取源码内容。详细过程如下。
为生成的代码文件设置构建标签#
代码路径:vendor/k8s.io/gengo/args/args.go
func Default() *GeneratorArgs {
return &GeneratorArgs{
GeneratedBuildTag: "ignore_autogenerated",
}
}
func (g *GeneratorArgs) NewBuilder() (*parser.Builder, error) {
b := parser.New()
// Ignore all auto-generated files.
b.AddBuildTags(g.GeneratedBuildTag)
// ...
}在 Default 函数中定义了默认的 GeneratedBuildTag 字符串,每次构建时,代码生成器会将 GeneratedBuildTag 字符串作为构建标签添加到生成的代码文件中。每一个代码生成器都会通过 Packages 函数执行该操作。以 deepcopy-gen 生成器为例,代码示例如下。
代码路径:vendor/k8s.io/gengo/examples/deepcopy-gen/generators/deepcopy.go
func Packages(...) generator.Packages {
header := append([]byte(fmt.Sprintf("// +build !%s\n\n", arguments.GeneratedBuildTag)), boilerplate...)
// ...
}deepcopy-gen 代码生成器中的 Packages 函数将 GeneratedBuildTag 字符串进行拼接,每一个通过 deepcopy-gen 代码生成器生成的代码文件 (zz_generated.deepcopy.go),第一行总是构建标签。生成构建标签的效果如下。
$ head -n 1 pkg/apis/abac/v1beta1/zz_generated.deepcopy.go
// +build !ignore_autogenerated!ignore_autogenerated 在 Kubernetes 中表示该文件是由代码生成器自动生成的,不需要人工干预或人工编辑该文件。
收集Go包信息并读取源码内容#
代码路径:vendor/k8s.io/gengo/args/args.go
func (g *GeneratorArgs) NewBuilder() (*parser.Builder, error) {
// ...
for _, d := range g.InputDirs {
var err error
if strings.HasSuffix(d, "/...") {
err = b.AddDirRecursive(strings.TrimSuffix(d, "/..."))
} else {
err = b.AddDir(d)
}
}
return b, nil
}代码生成器通过 --input-dirs 参数指定传入的 Go 包路径,通过 build.Import 函数收集 Go 包的信息。build.Import 函数支持多种模式,其中,build.ImportComment 用于解析 import 语句后的注释信息;build.FindOnly 用于查找 Go 包所在的目录,但不读取其中的源码的内容。代码函数层级为 b.AddDir -> b.importPackage -> b.addDir,代码示例如下。
代码路径:vendor/k8s.io/gengo/parser/parse.go
func (b *Builder) importBuildPackage(dir string) (*build.Package, error) {
buildPkg, err := b.importWithMode(dir, build.ImportComment)
if buildPkg == nil {
buildPkg, err = b.importWithMode(dir, build.FindOnly)
}
// ...
}通过 build.Import 函数获取 Go 包信息后,就可以得到 Go 包下的所有源码文件路径,将所有 Go 源码内容读入内存,等待 Lexer 进行下一步处理,代码示例如下。
func (b *Builder) addDir(dir string, userRequested bool) error {
// ...
for _, n := range buildPkg.GoFiles {
if !strings.HasSuffix(n, ".go") {
continue
}
absPath := filepath.Join(buildPkg.Dir, n)
data, err := ioutil.ReadFile(absPath)
err = b.addFile(pkgPath, absPath, data, userRequested)
}
return nil
}代码解析#
Go 语言的优势在于它是一种静态类型的语言,语法简单,与动态类型的语言相比,其特性更简单些。幸运的是,Go 语言标准库支持代码解析功能,而 Kubernetes 在该基础上进行了封装。代码解析流程可以分为 3 步:

首先,通过 Go 语言标准库 go/tokens 提供的 Lexer 将代码文本进行词法分析,得到 Tokens。然后,通过 Go 语言标准库 go/parser 和 go/ast 将 Tokens 构建为抽象语法树。最后,通过 Go 语言标准库 go/types 下的 Check 方法进行抽象语法树类型检查。
Lexer 词法分析器#
Go 语言标准库 go/tokens 提供词法分析器 (Lexical Analyzer,简称为 Lexer,也被称为扫描器)。词法分析器将字符序列转换为单词序列 (Tokens 或 Token List),其工作原理是对输入的代码文本进行词法分析,将一个个字符按照从左到右的顺序读入,根据构词规则识别单词,最终得到 Tokens。Token 是语言中的最小单位,它可以是变量、函数、运算符或数字。
以 x * i + 1 文本表达式为例,通过 Lexer 处理后得到 Token List。Lexer 的词法分析过程如下:

Parser 解析器#
通过 Lexer 得到 Token List 以后,它将被传递给 Parser 解析器。Parser 解析器将 Token List 转换为抽象语法树。抽象语法树也被称为语法树 (Syntax Tree),是编程语言源码的抽象语法结构的树状表现形式,树上的每个节点都表示源码中的一种结构。可以通过 Go 标准库 go/ast 打印出完整的抽象语法树结构。
抽象语法树是源码的结构化表示。通过抽象语法树,能够看到程序结构,如函数和常量声明。可以通过Go标准库go/ast打印出完整的抽象语法树结构。

Type-Checking 类型检查#
通过 Parser 解析器得到抽象语法树后,需要对抽象语法树中定义和使用的类型进行检查。对抽象语法树的节点进行遍历,在每个节点上对当前子树的类型进行检查,进而保证不会出现类型错误。通过 Go 标准库 go/types 下的 Check 方法可以进行抽象语法树检查。
另外,遍历抽象语法树有多种方式,如 DFS(深度优先搜索)遍历和 BFS(广度优先搜索)遍历。
代码解析过程实现#
实现代码解析需要通过 Lexer 词法分析器、Parser 解析器和 Type-Checking 类型检查。理解了上面的内容后,下面我们来学习 gengo 的代码解析实现,代码示例如下。
代码路径:vendor/k8s.io/gengo/parser/parse.go
func New() *Builder {
c := build.Default
return &Builder{
context: &c,
fset: token.NewFileSet(),
}
}
func (b *Builder) addFile(...) error {
klog.V(6).Infof("addFile %s %s", pkgPath, path)
p, err := parser.ParseFile(b.fset, path, src, parser.DeclarationErrors|parser.ParseComments)
// ...
}首先,token.NewFileSet 实例化得到 token.FileSet 对象,该对象用于记录文件中的偏移量、类型、原始字面量,以及词法分析的数据结构和方法等。得到 Tokens 后,在 addFile 函数中,使用 parser.ParseFile 对 Token 数据进行处理,Parser 解析器传入两种标识,其中,parser.DeclarationErrors 表示报告声明错误,parser.ParseComments 表示解析代码中的注释并将它们添加到抽象语法树。最终得到抽象语法树结构。
得到抽象语法树后,可以对抽象语法树进行类型检查,通过 Go 语言标准库 go/types 下的 Check 方法进行检查,检查过程中会进行一些优化,使程序的执行更快,代码示例如下。
func (b *Builder) typeCheckPackage(pkgPath importPathString) (*tc.Package, error) {
c := tc.Config{
IgnoreFuncBodies: true,
Importer: importAdapter{b},
Error: func(err error) {
klog.V(2).Infof("type checker: %v\n", err)
},
}
pkg, err := c.Check(string(pkgPath), b.fset, files, nil)
b.typeCheckedPackages[pkgPath] = pkg
return pkg, err
}类型系统#
gengo 的类型系统 (Type System) 在 Go 语言本身的类型系统归类的基础上添加了几种类型。gengo 的类型系统在 Go 语言标准库 go/types 基础上进行了封装。gengo 的类型系统提供了以下类型。
代码路径:vendor/k8s.io/gengo/types/types.go
type Kind string
const (
Builtin Kind = "Builtin"
Struct Kind = "Struct"
Map Kind = "Map"
Slice Kind = "Slice"
Pointer Kind = "Pointer"
Alias Kind = "Alias"
Interface Kind = "Interface"
Array Kind = "Array"
Chan Kind = "Chan"
Func Kind = "Func"
DeclarationOf Kind = "DeclarationOf"
Unknown Kind = "Unknown"
Unsupported Kind = "Unsupported"
Protobuf Kind = "Protobuf"
)
type Signature struct {
Receiver *Type
Parameters []*Type
ParameterNames []string
Results []*Type
ResultNames []string
Variadic bool
CommentLines []string
}所有的类型都通过 vendor/k8s.io/gengo/parser/parse.go 的 walkType 方法进行识别。在这里介绍下 gengo 与 Go 语言不同的类型,如 Builtin、Alias、DeclarationOf、Unknown、Unsupported 和 Protobuf。另外,Signature 并不是一种类型,它依赖于 Func 函数类型,用来描述 Func 函数的接收参数信息和返回值信息等。
1) Builtin 内置类型 Builtin 将多种 Base 类型归类成一种类型。以下几种类型在 gengo 中统称为 Builtin 类型。
- 内置字符串类型——
string。 - 内置布尔类型——
bool。 - 内置数字类型——
int、float、complex64等。
2) Alias 别名类型 Alias 别名类型是 Go 1.9 版本中支持的特性,代码示例如下。
type T1 struct{}
type T2 = T1这里的 T2 相当于 T1 的别名。但在 Go 语言标准库的 reflect (反射) 包识别 T2 原始类型时,它是 Struct 类型,无法识别它是 Alias 类型。gengo 依赖于 go/types 的 Named 类型,在声明时将 TypeName 对象绑定到 Named 类型。
3) DeclarationOf 声明类型 DeclarationOf 并不是严格意义上的类型,它是一种声明过的函数、全局变量或常量,但并未被引用过,代码示例如下。
代码路径:pkg/apis/abac/v1beta1/register.go
AddToScheme = localSchemeBuilder.AddToScheme例如,在 register.go 中,AddToScheme 变量被声明后未被其他对象使用,则可以认为它是 DeclarationOf 类型的。
4) Unknown——未知类型 当对象与以上所有类型都不匹配时,它就是 Unknown 类型。
5) Unsupported——未支持类型 当对象属于 Unknown 类型时,会将该对象设置为 Unsupported 类型,并且在使用过程中会报错。
6) Protobuf——protobuf类型 由 go-to-protobuf 代码生成器单独处理。
代码生成#
编译器的代码生成一般生成的是二进制代码,而 Kubernetes 的代码生成器生成的是 Go 语言代码。首先了解一下 gengo 的 Generator 接口,接口定义如下。
代码路径:vendor/k8s.io/gengo/generator/generator.go
type Generator interface {
Name() string
Filter(*Context, *types.Type) bool
Namers(*Context) namer.NameSystems
Init(*Context, io.Writer) error
Finalize(*Context, io.Writer) error
PackageVars(*Context) []string
PackageConsts(*Context) []string
GenerateType(*Context, *types.Type, io.Writer) error
Imports(*Context) []string
Filename() string
FileType() string
}Generator 接口的说明如下:
- Name:代码生成器的名称,返回值为生成的目标代码文件名的前缀,如 deepcopy-gen 代码生成器的目标代码文件名为
zz_generated.deepcopy。 - Filter:类型过滤器,过滤不符合当前代码生成器所需的类型。
- Namers:命名管理器,支持创建不同类型的命名,如根据类型生成名称。
- Init:代码生成器生成代码之前的初始化操作。
- Finalize:代码生成器生成代码之后的收尾操作。
- PackageVars:生成全局变量代码块,如
var(...)。 - PackageConsts:生成常量代码块,如
consts(...)。 - GenerateType:生成代码块,根据传入的类型生成代码。
- Imports:获得需要生成的 import 代码块,通过该方法生成 Go 语言的 import 代码块。如
import(...)。 - Filename:生成的目标代码文件的全名,如 deepcopy-gen 代码生成器的目标代码文件的全名为
zz_generated.deepcopy.go。 - FileType:生成代码文件的类型,一般为 golang,也有 protoidl、api-violation 等代码文件类型。
Kubernetes 目前提供的每个代码生成器都实现了以上接口。如果代码生成器没有实现某些接口,则继承默认生成器 (DefaultGen) 的方法,DefaultGen 定义在 vendor/k8s.io/gengo/generator/default_generator.go 中。
以 deepcopy-gen 代码生成器为例,详细讲解其代码生成原理,命令如下。
$hack/make-rules/build.sh k8s.io/code-generator/cmd/deepcopy-gen$ ./hack/run-in-gopath.sh ./_output/bin/deepcopy-gen \
--v 1 \
--logtostderr \
-i "k8s.io/kubernetes/pkg/apis/abac/v1beta1" \
--bounding-dirs k8s.io/kubernetes,"k8s.io/api" \
-O zz_generated.deepcopy首先,通过 build.sh 脚本手动构建 deepcopy-gen 代码生成器二进制文件。然后,将需要生成的包 k8s.io/kubernetes/pkg/apis/abac/v1beta1 作为 deepcopy-gen 的输入源,内部进行一系列解析,最终通过 -O 参数,生成名为 zz_generated.deepcopy 的代码文件。

实例化 generator.Packages 对象#
deepcopy-gen 代码生成器根据输入的包的目录路径 (输入源) 实例化 generator.Packages 对象,根据 generator.Packages 结构生成代码,代码示例如下。
代码路径:vendor/k8s.io/gengo/examples/deepcopy-gen/generators/deepcopy.go
packages = append(packages,
&generator.DefaultPackage{
PackageName: strings.Split(filepath.Base(pkg.Path), ".")[0],
PackagePath: path,
HeaderText: header,
GeneratorFunc: func(c *generator.Context) (generators []generator.Generator) {
return []generator.Generator{
NewGenDeepCopy(...),
}
},
FilterFunc: func(c *generator.Context, t *types.Type) bool {
return t.Name.Package == pkg.Path
},
})在 deepcopy-gen 代码生成器的 packages 函数中,实例化 generator.Packages 对象并返回该对象。根据输入源信息,实例化当前 packages 对象,使其 PackageName 字段为 v1beta1,PackagePath 字段为 k8s.io/kubernetes/pkg/apis/abac/v1beta1。其中,最主要的 GeneratorFunc 字段定义了 Generator 接口的实现 (NewGenDeepCopy 实现了 Generator 接口方法)。
执行代码生成#
在 gengo 中,generator 定义代码生成器通用接口 Generator。通过 ExecutePackage 函数,调用不同代码生成器 (例如 deepcopy-gen) 的 Generator 接口方法,执行代码生成,代码示例如下。
代码路径:vendor/k8s.io/gengo/generator/execute.go
func (c *Context) ExecutePackage(outDir string, p Package) error {
for _, g := range p.Generators(packageContext) {
genContext := packageContext.filteredBy(g.Filter)
f := files[g.Filename()]
if f == nil {
f = &File{
Name: g.Filename(),
FileType: fileType,
PackageName: p.Name(),
Header: p.Header(g.Filename()),
Imports: map[string]struct{}{},
}
files[f.Name] = f
}
if vars := g.PackageVars(genContext); len(vars) > 0 {
// ...
}
if consts := g.PackageConsts(genContext); len(consts) > 0 {
// ...
}
if err := genContext.executeBody(&f.Body, g); err != nil {
return err
}
if imports := g.Imports(genContext); len(imports) > 0 {
// ...
}
}
// ...
err = assembler.AssembleFile(f, finalPath)
}ExecutePackage 函数生成代码的执行流程为:生成 Header 代码块 → 生成 Imports 代码块 → 生成 Vars 全局变量代码块 → 生成 Consts 常量代码块 → 生成 Body 代码块。最后,调用 assembler.AssembleFile 函数,将生成的代码块信息写入 zz_generated.deepcopy.go 文件。生成的代码结构图如下:

deepcopy-gen 代码生成器最终生成代码文件 zz_generated.deepcopy.go,该文件的整体结构可分为以下部分:
- Header:代码块信息,包括 build tag 和 license boilerplate 文件 (存放开源软件作者及开源协议等信息)。
- Imports:代码块信息,引入外部包。
- Vars:全局变量,当前代码文件未使用 Vars。
- Consts:常量代码块信息,当前代码文件未使用 Consts。
- Body:代码块信息,生成的 DeepCopy 函数。
在生成代码的过程中,Filter 函数和 GenerateType 函数非常重要。deepcopy-gen 代码生成器根据 Filter 类型过滤器筛选哪些结构是需要生成的,deepcopy-gen 的 Filter 类型过滤器实现如下。
代码路径:vendor/k8s.io/gengo/examples/deepcopy-gen/generators/deepcopy.go
func copyableType(t *types.Type) bool {
if t.Kind != types.Struct {
return false
}
return true
}可以看到 Filter -> copyableType 的实现,deepcopy-gen 代码生成器只筛选出类型为 Struct 的数据 (只为 Struct 结构的数据生成 DeepCopy 函数)。
GenerateType 函数根据传入的类型生成 Body 代码块信息。内部通过 Go 语言标准库 text/template 模板语言渲染出生成的 Body 代码块信息,代码示例如下。
func (g *genDeepCopy) GenerateType(...) error {
// ...
sw := generator.NewSnippetWriter(w, c, "$", "$")
args := argsFromType(t)
if deepCopyMethodOrDie(t) == nil {
sw.Do("// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new $.type|raw$.\n", args)
if isReference(t) {
sw.Do("func (in $.type|raw$) DeepCopy() $.type|raw$ {\n", args)
} else {
sw.Do("func (in *$.type|raw$) DeepCopy() *$.type|raw$ {\n", args)
sw.Do("if in == nil { return nil }\n", nil)
}
}
return sw.Error()
}generator.NewSnippetWriter 内部封装了 text/template 模板语言,通过将模板应用于数据结构来执行模板。SnippetWriter 对象在实例化时传入模板指令的标识符 (指令开始为 $,指令结束为 $,有时也会使用 {{}} 作为模板指令的标识符)。
SnippetWriter 通过 Do 函数加载模板字符串,并且执行渲染模板。模板指令中的点 (.) 表示引用 args 参数并将其传递到模板指令中。模板指令中的 | 表示管道符,将左边的值传递给右边。

