1. gengo代码生成器
Kubernetes 的代码生成器都是在k8s.io/gengo 包的基础上实现的。上一篇介绍了deepcopy-gen 、defaultcr-gen、convers lOn-gen , openapl-gen 、go-bi ndata 等代码生成器的用法。
代码生成器都会通过一个输入包路径(–input-dirs) 参数,根据 gengo 的词法分析、抽象语法树等操作,最终生成代码并输出( –output-file-base )。
gengo 代码目录结构如下:
- args : 代码生成器的通用 flags 参数。
- examples : 包含 deepcopy-gen 、defaulter-gen 、import-boss 、set-gen 等代码生成器的生成逻辑。
- generator : 代码生成器通用接口Generator 。
- namer : 命名管理,支持创建不同类型的名称。例如,根据类型生成名称,定义type foo string,能够生成 func FooPrinter(f * foo) { Print(string(* f)) }
- parser : 代码解析器,用来构造抽象语法树。
- types : 类型系统,用于数据类型的定义及类型检查算法的实现。
- boilerplate : 版权信息,该目录下存放了两个txt文件,一个是空白文件,一个是版权信息文件,
gengo 的代码生成逻辑与编译器原理非常类似,大致可分为如下几个过程:
- Gather The lnfo : 收集Go 语言源码文件信息及内容。(go/build)
- Lcxer/Parser : 通过Lexer 词法分析器进行一系列词法分析。(go/token)
- AST Generatol : 生成抽象语法树。(go/ast)
- Type C hecker : 对抽象语法树进行类型检查。(go/types)
- Code Generation : 生成代码,将抽象语法树转换为机器代码。
2. gengo入口流程分析
在 gengo 项目中,examples 目录中给了一些自定生成代码的使用实例,例如 deepcopy-gen、defaulter-gen 等。现在以 deepcopy-gen 生成器为例,查看 gengo 的流程,下面过程是在 k8s.io\gengo\examples\deepcopy-gen\main.go 文件中:
- 初始化 GeneratorArgs。GeneratorArgs 结构体表示代码生成器的一些参数,例如需要解析的源文件、生成文件的版权信息等等。
- 生成自定义参数 CustomArgs,并且将自定义参数 CustomArgs 赋值到 GeneratorArgs 参数集。CustomArgs.BoundingDirs 表示只处理这些目录下的根类型。
- 执行 Execute() 方法,代码生成器的入口函数。Execute() 是 GeneratorArgs 结构体的方法。
k8s.io\gengo\examples\deepcopy-gen\main.go 文件源代码如下:
3. gengo各个步骤源码分析
3.1 收集Go 包信息
Go 语言标准库提供了go/build package,该包支持 Go 语言的构建标签 (Build Tag ) 机制来构建约束条件 (Build Constraint) 。Go 语言的条件编译有两种定义方法,分别是:
- 构建标签 : 在源码里添加注释信息,比如 // +build linux , 该标签决定了源码文件只在Linux 平台上才会被编译。
- 文件后缀 : 改变 Go 语言代码文件的后缀,比如 foo_inux.go,该后缀决定了源码文件只在Linux 平台上才会被编译。
gengo 就是用到了 go/build package 来手机 Go 源码信息。gengo 收集 Go 包信息可分为两步:
- 第 1 步,为生成的代码文件设置构建标签
- 第 2 步,收集 Go 包信息并读取源码内容
3.1.1 为主成的代码文件设置构建标签
在第2小节中,分析了gengo 入口的过程,其中第一步,就是初始化 GeneratorArgs 对象, GeneratorArgs 结构体就是传递给生成器的所有参数。下面是 GeneratorArgs 结构体的属性列表:
- InputDirs []string – 需要解析的源文件目录
- OutputBase string – 生成文件的基础目录
- OutputPackagePath string – 生成文件的 package 路径
- OutputFileBaseName string – 生成代码的文件名BaseName
- GoHeaderFilePath string – 文件头,也就是版权信息的文件路径
- GeneratedByCommentTemplate string – 在文件开头生成 // Code generated by … 的注释信息。注释信息中的”GENERATOR_NAME” 将被替换为代码生成器的名称
- VerifyOnly bool – VerifyOnly 为 true ,则只校验,不生成任何代码
- IncludeTestFiles bool – 是否包含测试文件
- GeneratedBuildTag string – GeneratedBuildTag 就是 tag 名称,每个生成器都有自己独特的 Tag 名称
- CustomArgs interface{} – 自定义参数
- defaultCommandLineFlags bool – 是否使用默认的 Command Line 标志
在 k8s.io\gengo\examples\deepcopy-gen\main.go 函数中的第一步,初始化使用了 arguments := args.Default() 。在 Default 函数中定义了默认的 Gene ratedBuildTag 字符串,在每次构建时,代码生成器会将 GeneratedBuildTag 作为构建标签打入生成的代码文件中。初始化 Default() 函数的方法如下(代码路径为:k8s.io\gengo\args\args.go):
初始化并且设置了自定义参数后,GeneratorArgs 参数就设置好了,这时候,每个代码生成器都会通过 Packages 函数将具体生成器的 GeneratorArgs 对象中的 Tags 修改成对应和生成器的 Tags。最后执行 GeneratorArgs 结构体的 Execute 方法,正式进入代码生成器的核心逻辑。
3.1.2 收集Go 包信息并读取源码内容
上面我们介绍了 GeneratorArgs 结构体的初始化,下面我们来看一下入口程序 Execute 方法都做了哪些操作。这里先不介绍传入的参数,先大致看一下 Execute 方法的主要功能:
- 如果开启了命令行模式,那么就添加命令行选项(3.1.2)
- 创建代码生成器,并且初始化(3.1.2)
- 为改代码生成器创建一个全局的 Context 上下文环境(3.3.3)
- 生成 Packages 列表(3.3.4)
- 根据每一个 Package 解析并且生成最终的代码(3.3.5)
Execute 函数源代码如下,代码路径为:k8s.io\gengo\args\args.go 。
第一步,添加命令行选项。这里用的 pflag 来添加命令行选项,默认的命令行选项一共有 7 条,分别是:
- –input-dirs(-i) : 逗号分隔的列表,导入路径以从中获取输入类型
- –output-base(-o) : 输出的基础路径,如果 GOPATH 设置了,默认为 $GOPATH/src/,否则为 ./
- –output-package(-p) : 输出的 package 路径
- –output-file-base(-O) : 自动生成代码文件的名称,没有 .go 后缀
- –go-header-file(-h) : 生成文件的头信息,也就是版权信息
- –verify-only : 如果为 true,则只验证不生成代码
- –build-tag : 用于识别需要生成代码文件的 Tags 信息。
AddFlags 函数的功能就是添加命令行选项,代码路径在 k8s.io\gengo\args\args.go,源码如下:
Execute 函数的第二步,创建代码生成器,并且初始化。GeneratorArgs 结构体的 NewBuilder 方法来创建代码生成器。
下面看一下 b, err := g.NewBuilder() 这里的 NewBuilder 函数,其主要作用就是收集 Go 包信息并读取源码内容。步骤为:
- 通过 go/build package 生成代码生成器,用来收集 Go Package 的信息。
- 将参数列表 GeneratorArgs 结构体中的 GeneratedBuildTag 字段,添加到 go/build 的 Context 中。
- 代码生成器通过 –input-dirs 参数指定传入的 G0 包路径,通过 build.Import 方法收集 G0 包的信息。这里 AddDir() 和 AddDirRecursive() 两个函数的主要作用是一样的,都是通过 build.Import 方法来获取 Go Package 的详细信息,唯一的区别就是 AddDirRecursive 函数是可以递归将其子目录也进行遍历。
NewBuilder 函数的源码如下,代码路径为:k8s.io\gengo\args\args.go。
b := parser.New() 是代码生成器的初始化,使用 go/build package 来获取到 Go Package 的信息。New 函数返回 Builder 对象,Builder 结构体主要属性为 context *build.Context ,build.Context 是在 build 过程中使用到的上下文信息,例如:GOROOT、GOPATH 等环境变量,BuildTags 和 ReleaseTags 列表。
Builder 对象的属性如下所示,代码目录为:k8s.io\gengo\parser\parse.go。parser.New() 就是将 Builder 各个字段初始化后返回。
接下来看一下 AddDir 和 AddDirRecursive 的区别。两个函数的功能是一样的,扫描传入参数的文件夹,获取所有 .go 文件的信息。传入参数 dir 应该是一个 package 的目录,如果这个目录下不是一个 package 会搜索 GOROOT、GOPATH 和 which go 的位置目录。
AddDirRecursive 函数相比于 AddDir 唯一区别是会递归搜索子目录,实现很简单,就是利用 filepath.Walk 函数,进行遍历每个子目录。代码目录为:k8s.io\gengo\parser\parse.go
无论是 AddDir 函数还是 AddDirRecursive 函数,都调用了 importPackage 函数,该函数的调用过程为:
- importPackage 函数,功能:如果当前路径的包没有获取 Package 信息,那么就先调用 addDir 函数,获取 Package 信息,并生成 AST 语法树,然后执行类型检查 typeCheckPackage 如果当前路径已经获取了 Package 信息,那么就直接进行类型检查
- addDir 函数,功能:生成 Package 信息,并且生成 AST 语法树
- importBuildPackage 函数,功能:利用 go/build Context.Import 函数,生成 Package 信息,并且添加到 Builder 对象中
- addFile 函数,功能:读取每个 Go 文件内容,将其内容转成 AST 语法树,下一节介绍
- typeCheckPackage 函数,功能:类型检查
- addDir 函数,功能:生成 Package 信息,并且生成 AST 语法树
下面为 获取 Go Package 信息的几个函数的源码,代码目录为:k8s.io\gengo\parser\parse.go 。
importPackage 函数的调用顺序为:importPackage -> addDir -> importBuildPackage -> importWithMode -> go/build Context.Import
最终生成的 Package 信息保存在代码生成器的 buildPackages 属性中。
3.2 代码解析
Go 语言的优势在于它是一个静态类型语言,语法很简单,与动态类型语言相比更简单一些。幸运的是,Go 语言标准库支持代码解析功能,而 Kubemetes 在该基础
上进行了功能封装。代码解析流程可分为3 步。gengo 代码解析流程为:
- 第1 步,通过标准库 go/tokens 提供的 Lexer 词法分析器对代码文本进行词法分析,最终得到 Tokens
- 第2 步, 通过标准库 go/parser 和 go/ast 将 Tokens 构建为抽象语法树 (AST )
- 第3 步,通过标准库 go/types 下的Check 方法进行抽象语法树类型检查,完成代码解析过程。
gengo 的代码解析过程与 go build 的代码解析过程相同,可以参考 Golang – 编译器原理 。
上面我们分析了 gengo 的入口程序,k8s.io\gengo\examples\deepcopy-gen\main.go,其中主要的执行函数为 arguments.Execute() ,该函数体在 k8s.io\gengo\args\args.go 文件中定义,并且也分析了 Execute 函数的第一步添加命令行选项,和第二步初始化并获取 Go Package 信息得到代码解析器 Builder 对象的过程。
其中在 Execute 函数的第二步中,调用 g.NewBuilder() 时,通过 parser.New() 会初始化一个 Builder 对象,该 Builder 对象中有一个属性为: fset *token.FileSet。该属性就是 Lexer 词法分析器。
- 首先,通过 token.NewFileSet 实例化得到 token.FileSet 对象,该对象用于记录文件中的偏移量、类型、原始字面盘及词法分析的数据结构和方法等,Lexer 词法分析器会最终生成 Token 序列数据。
- 得到 Tokens 后,在 addFile 函数中,使用 parser.ParseFile 解析器对 Tokens 数据进行处理,最终生成 AST 抽象语法树。
解析器的调用过程为:main -> Execute -> NewBuilder -> AddDir/AddBuildTags -> importPackage -> addDir(获取 Package 信息并解析)/typeCheckPackage(类型检查) -> importBuildPackage(获取 Package 信息)/addFile(词法解析并生成 AST 抽象语法树)
下面就分析一下词法解析和 AST 抽象语法树。
3.2.1 词法分析以及生成 AST 抽象语法树
首先,通过 token.NewFileSet 实例化得到 token.FileSet 对象,该对象用于记录文件中的偏移量、类型、原始字面量及词法分析的数据结构和方法等,Lexer 词法分析器生成 Token 序列数据。得到 Tokens 后,再根据 Tokens 生成 AST 抽象语法树,整个过程利用 go/token、go/parser 两个标准库实现。其中最关键的函数是:
func ParseFile(fset *token.FileSet, filename string, src interface{}, mode Mode) (f *ast.File, err error)
ParseFile 函数的作用:解析单个 Go 源文件的源代码,并返回对应的 ast.File 节点。ast.File 就是生成的 AST 抽象语法树
ParseFile 函数参数:
- fset *token.FileSet : go/token 中的 FileSet 表示一组源文件。用于将源码内容转换成的 Token 内容
- filename string : 源码文件路径,如果 src 为 nil,那么就根据 filename 来解析
- src interface{} : 源码文件内容,如果传入了 src,优先从 src 解析文件
- mode Mode : 解析模式,它们控制解析的源代码数量和其他可选的解析器功能。可选的有:
- PackageClauseOnly – 在解析当前 Package 后停止
- ImportsOnly – 在解析 import 语句后停止
- ParseComments – 解析注释并将它们添加到 AST
- Trace – 打印已解析的 Trace 信息
- DeclarationErrors – 报告错误声明
- SpuriousErrors – 与 AllErrors 相同
- AllErrors – 报告所有错误
解析文件的功能在 addFile 函数中定义,代码路径为 k8s.io\gengo\parser\parse.go。源码如下:
3.2.2 类型检查
以上,我们分析完了文件解析成 AST 抽象语法树,上面提到了在 importPackage 有两个作用:
- 生成 Package 信息,并且进行文件解析
- 类型检查
gengo 的类型系统 (Type System)在 Go 语言本身的类型系统之上归类并添加了几种类型。 gengo 的类型系统在 Go 语言标准库 go/types 的基础上进行了封装。
go/types 是 Go 语言程序的类型检查器,go/types 标准库的使用方法是:
- 先初始化一个 Config 对象,Config 对象有以下几个属性:
- IgnoreFuncBodies – 如果设置为 true,则函数体内不进行类型检查
- FakeImportC – 不要轻易使用,`import “C”`(对于需要 Cgo 的包)声明一个空的“C”包,并且对于引用 C 包的限定标识符会忽略错误。
- Error – 如果有错误,那么就会调用这个错误处理函数
- Importer – 必须的,如果没有 Importer 导入器,会报错,Importer 就是从 import 声明语句导入引用的包
- Sizes – 如果设置了 Sizes,会为 unsafe 包提供一个动态调整大小的函数
- DisableUnusedImportCheck – 如果设置为 true,则不会检查没有使用的类型
- 初始化 Config 对象后,调用 Config.Check 函数,进行类型检查。返回 Package 对象,其中 Package 对象中有 complete 属性,如果为 true 表示类型检查完成,否则表示类型检查有错误发生。Check 函数的参数有:
- path string – 包路径
- fset *token.FileSet – go/token 生成的文件集 *token.FileSet
- files []*ast.File – AST 抽象语法树的每个文件节点信息,对应 fset
- info *Info – Info 保存类型检查包的结果类型信息。如果包有类型错误,收集的信息可能不完整,一般传入 nil
下面就是 typeCheckPackage 类型检查函数的源码,代码路径为:k8s.io\gengo\parser\parse.go。
3.3 代码生成
3.3.1 类型系统
在代码生成之前,我们先来看一下 gengo 的类型系统, k8s.io/gengo/types/types.go 文件中定义了 k8s 的所有类型,最后使用了 go/types 标准库将所有的类型生成 go/types 标准库的 Package 类型放入全局的 Context 上下文中。
所有的类型都通过 k8s .io/gengo/parser/parse.go 的 walkType 方法进行识别。gengo 类型系统中的 Struct、 Map 、 Pointer 、 lnterface 等,与 g0 语言提供的类型并无差别。
下面介绍一下 gengo 与 g0 语言不同的类型,例如 Builtin 、 Alias 、 Declaratio110f、UnKn0wn , Unsupp0rted 及 Protobuf。另外, Sigl1ature 并非是一个类型,它依赖于 Func 函数类型,用来描述 Func 函数的接收参数信息和返回值信息等。
1. Builtin(内置类型)
Builtin 将多种 Base 类型归类成一种类型,以下几种类型在gengo 中统称为 Builtin 类型。
- 内置字符串类型 – string 。
- 内置布尔类型 – bool .
- 内置数字类型 – int、uint、float、byte、uintptr 等.
2. Alias( 内置类型)
Alias 就是类型别名,例如:
type T1 struct{)
type T2 T1
代码第2 行,通过等于(=)符号, 基于一个类型创建了一个别名。这里的 T2 相当于 T1 的别名。但在 Go 语言标准库的 reflect (反射)包识别 T2 的原始类型时,会将它识别为Struct 类型,而无法将它识别为 Alias 类型。
如何让Alias 类型在运行时可被识别呢? 答案是因为 gengo 依赖于 go/types 的 Named 类型,所以要让 Alias 类型在运行时可被识别, 在声明时将 TypeName 对象绑定到 Named 类型即可。Named 表示命名变量。
3. DeclarationOf(声明类型)
DeclarationOf 并不是严格意义上的类型,它是声明过的函数、全局变量或常量,但并未被引用过。
4. Unknown(未知类型)
当对象匹配不到以上所有类型的时候, 它就是Unknown 类型的。
5. Unsupported(未支持类型)
当对象属于Unknown 类型时,则会设置该对象为Unsupported 类型,并在其使用过程中报错。
6. Protobuf(Protobuf 类型)
由 go-to-protobuf 代码生成器单独处理的类型。
那么这些类型是怎么存储的呢,k8s.io/gengo/types/types.go 文件中定义了 Type 结构体,该结构体可以表示任何类型结构。源码如下:
3.3.2 命名系统
gengo 的命名系统,就是自动生成代码文件的名称怎么定义,生成的函数名,类型名怎么命名。代码路径为:k8s.io\gengo\namer\namer.go。
gengo 的命名系统定义了一个常规的 NameStrategy 结构体,该结构体中包括:
- Prefix, Suffix string – 命名的前缀后缀
- Join func(pre string, parts []string, post string) string – 怎么拼接,拼接的过程函数
- IgnoreWords map[string]bool – 命名时需要忽略的一些字符串包名
- PrependPackageNames int – 表示精确的添加多少个包的目录名
- Names – 所有类型名称的缓存
NameStrategy 结构体有一个 Name 方法,根据 Kind 的不同类型,返回生成的不同名称。
3.3.3 为 Builder 创建全局的 Context 上下文
3.1 和 3.2 小节介绍了 k8s.io\gengo\args\args.go 中 Execute 函数的前两步,下面我们来看Execute 函数的第三步,为改代码生成器创建一个全局的 Context 上下文环境。在代码生成前,给 Builder 对象创建了一个全局 Context 上下文:
c, err := generator.NewContext(b, nameSystems, defaultSystem)
参数就是初始化了一个命名系统,默认命名系统为 public,就是刚刚介绍的 NameStrategy 结构体定义的通用命名系统。
NewContext 就是为代码生成器 Builder 创建一个全局的 Context 上下文对象,这个 Context 结构体如下,源码路径 k8s.io\gengo\generator\generator.go。
NewContext 的作用主要有两个:
- 通过 FindTypes 方法,获取所有类型的 Package 的信息
- 创建一个全局的 Context 对象,并且赋值
源码如下,代码路径为: k8s.io\gengo\generator\generator.go
3.3.4 解析注释信息,生成 Packages 列表
k8s.io\gengo\args\args.go 的 Execute 函数的第四步,生成 Packages 列表。在生成全局的 Context 上下文后,会执行一个 pkg 函数,返回一个 Packages 列表,这个函数的作用为:
- 加载通过 –go-header-file 指定的文件头注释信息,也就是版权信息。(通过 arguments.LoadGoBoilerplate() 函数)
- 在初始化全局 Context 时,会将通过类型检查 go/types Config.Check() 生成的 Package 转成 string 类型,添加到 Context.Inputs 属性列表中
- 生成文件头信息 header,也就是自动生成代码文件的 版权等信息。
- 将特定生成器的自定义选项内容取出,供以后使用。例如,deepcopy-gen 代码生成器有特定的选项 –bounding-dirs。
- 上面介绍了 NewContext 函数中使用 FindTypes 将所有用户指定的 package 信息生成 gengo/types 的 Pacakge 对象中,并且保存在 Universe 。现在遍历所有的 input package,做如下操作(deepcopy-gen 代码生成器为例):
- 先取出文件开头的注释信息,并且解析,将 “// +k8s:deepcopy-gen=xxx” 注释信息解析为 enabledTagValue 结构体对象
- 根据 = 后面的值进行判定,如果 = 后面的值为 “package” ,表示当前package 需要进行代码生成,pkgNeedsGeneration 设置为 true。并且判断源路径中是否有 /vendor/ 路径,如果有,将 path 设置为带有 /vendor 的路径。然后,将 结果放入 generator.Packages 中。
- 如果 = 后面的值不是 “package” ,或者文件开头就没有 “// +k8s:deepcopy-gen=xxx” 标签,那么将 pkgNeedsGeneration 设置为 false,并且对该 package 内的所有类型进行遍历(gengo/types 中的 Type),检查类型声明上方是否有”// +k8s:deepcopy-gen=true”的注释信息,如果有,再将pkgNeedsGeneration 设置为 true,跳出类型遍历
- 遍历结束后,返回 generator.Packages 对象,也就是对所有 package 的注释信息的 Tags 进行解析后的结果。
k8s.io\gengo\args\args.go 的 Execute 函数的调用方式为:
if err := arguments.Execute(generators.NameSystems(), generators.DefaultNameSystem(), generators.Packages,)
其中,最后一个参数就是对所有 package 的注释信息的 Tags 进行解析的函数, generators.Packages 函数的代码路径为:k8s.io\gengo\examples\deepcopy-gen\generators\deepcopy.go ,源码如下:
3.3.5 遍历 Packages 生成最终的代码
在 3.3.4 小节中,介绍了 generators.Packages() 函数,该函数主要作用就是解析注释信息中的 Tags,并且将需要代码生成的 Packages 返回,也就是带有 // +k8s:deepcopy-gen=xxx 注释信息的 Package 返回。
梳理一下 generators.Packages() 的返回值(代码路径为 :k8s.io\gengo\examples\deepcopy-gen\generators\deepcopy.go):
- generators.Packages() 函数的额返回值是 generator.Packages
- generator.Packages 是一个 generator.Package 列表,定义格式为: type Packages []Package
- generator.Package 是一个接口类型,只要实现了该接口中的所有方法的类型,都可以为该接口赋值
generators.Packages() 函数在满足带有 // +k8s:deepcopy-gen=xxx 注释信息后,会生成一个 generator.DefaultPackage 结构体对象,generator.DefaultPackage 结构体在 k8s.io\gengo\generator\default_package.go 定义,并且实现了generator.Package 接口定义的所有方法,因此可以赋值给该接口。
位于 k8s.io\gengo\generator\generator.go 文件的 generator.DefaultPackage 结构体,作用就是生成代码的过程中会用到,一共有 8 个属性:
- PackageName string – 包名称,生成的代码中,”package xxxx”
- PackagePath string – 包导入路径,也就是导入该包时的路径
- Source string – 包磁盘路径,该包在当前磁盘中的绝对路径
- HeaderText []byte – 每个文件顶部的注释信息
- PackageDocumentation []byte – 如果有 doc.go 文件,将该文件保存在该属性中
- GeneratorFunc func(*Context) []Generator – 生成代码的过程,返回 Generator 列表,Generator 又是一个接口类型。如果不为空,调用 GeneratorFunc ,如果为空,那么就使用静态列表 GeneratorList。
- GeneratorList []Generator – 静态的 Generator 列表。
- FilterFunc func(*Context, *types.Type) bool – 可选,筛选暴露给生成器的类型。
在 generator.DefaultPackage 属性中,会使用到 Generator 接口类型,Generator 接口一共定义了 11 个方法,分别是:
- Name() string : 代码生成器的名称 ,返回值为生成的目标代码文件名的前缀,例如 deepcopy-gen 代码生成器的目标代码文件名的前缀为 zz~enerated.deepcopy
- Filter(*Context, *types.Type) bool : 类型过滤器,过滤掉不符合当前代码生成器所需的类型。 如果生成器关心这个类型,Filter应该返回true。
- Namers(*Context) namer.NameSystems : 命名管理器,支持创建不同类型的名称。例如 ,根据类型生成名称。 也就是说,需要创建特殊的名称,在这里返回,否则返回nil
- Init(*Context, io.Writer) error : 代码生成器生成代码之前的初始化操作。
- Finalize(*Context, io.Writer) error : 代码’t成据生成代码之后的收尾操作。
- PackageVars(*Context) []string : 生成全局变量代码块,并且不包括前后的 \t 或 \n 符号,例如:var ( … )
- PackageConsts(*Context) []string : 生成常量代码块,并且不包括前后的 \t 或 \n 符号,例如 consts ( … ) 。
- GenerateType(*Context, *types.Type, io.Writer) error : 生成代码块。根据传入的特定类型生成代码。
- Imports(*Context) []string : 获得需要生成的 import 代码块。通过该方法生成 Go 语言的 Import 代码块,例如 import ( … ) 。
- Filename() string : 生成的目标代码文件的全名 , 例如 deepcopy-gen 代码生成器的目标代码文件名为 zz_generated.deepcopy.go 。
- FileType() string : 生成代码文件的类型,一般为 golang ,也有 protoidl 、api-violation 等文件类型
Kubemetes 目前提供的每个代码生成器都可以实现以上方法。如果代码生成器没有实现某些方法,则继承默认代码生成器(DefaultGen) 的方法, DefaultGen 定义于 k8s.io\gengo\generator\default_generator.go 中 。下面就是 default_package.go 和 default_generator.go 文件的源码分析:
在 3.3.4 实例化 generator.Packages 对象时,deepcopy- gen 代码生成器根据输入的包的目录路径〈即输入源) ,实例化 generator.Packages 对象,根据 generator. Packages 结构生成代码,代码示例如下: 代码路径 k8s.io\gengo\examples\deepcopy-gen\generators\deepcopy.go。
在 deepcopy-gen 代码生成器的 Packages 函数中,实例化 generator.Packages 对象并返回该对象。根据输入源信息 , 实例化当前 Packages 对象的结构。其中,最主要的是 GeneratorFunc 定义了 Generator 接口的实现(即 NewGenDeepCopy 返回的对象实现了 Generator 接口方法)。
NewGenDeepCopy 函数返回了一个 genDeepCopy 结构体,代码路径在:k8s.io\gengo\examples\deepcopy-gen\generators\deepcopy.go,这也是 deepcopy-gen 代码生成器重新添加的结构体,这个结构体是 deepcopy-gen 代码生成器专有的结构体,并且该结构体中继承了默认代码生成器 generator.DefaultGen 的所有方法,并且实现了属于 deepcopy-gen 代码生成器的 Generator 接口的几个方法来覆盖默认 generator.DefaultGen 的方法,最后,在代码生成的不同阶段,调用不同的方法。
到此为止,就差 Execute 函数中的最后一个步骤,遍历 Package 生成最终的代码。k8s.io\gengo\args\args.go 文件中的 Execute 函数执行生成代码的操作为:
c.ExecutePackages(g.OutputBase, packages)
逐步分析代码生成的过程:
- 调用 ExecutePackages 函数
- 遍历 packages,调用具体的 ExecutePackage 函数进行代码生成
- 调用 Generator 接口的各个方法,生成各个部分的代码片段,保存在 generator.File 结构体中:
- PackageVars – Generator 接口的方法,生成全局变量代码块信息,当前 deepcoy-gen 代码生成器未使用 Vars .
- PackageConsts – Generator 接口的方法,生成常量代码块信息, 当前 deepcoy-gen 代码生成器未使用 Consts .
- Imports – Generator 接口的方法,生成 import 代码块,引入外部包 。
- Header – 代码块信息,包括 build tag 和 license boilerplate 文件(存放开源软件作者及开源协议等信息) , 其中 license boilerplate 文件可以从 hacklboilerplate/boilerplate.go.txt 中获取.
- Body – Body 代码块信息,生成 DeepCopy 深复制函数 。
- 调用 AssembleFile 函数,将 generator.File 结构体中的代码片段,写入到文件中。
注意:deepcopy-gen 代码生成器没有生成 Consts 和 Vars 的代码片段,因此,deepcopy-gen 代码生成器就没有覆盖默认代码生成器的 PackageVars 、PackageConsts 方法,而是重写了 Imports 、GenerateType 方法用来自定义生成 import 代码段和 Body 代码段。
下面就来看一下具体代码生成过程的源码分析,也就是 Execute 函数的最后一步:、
ExecutePackage 代码生成执行流程· 生成 Header 代码块 -> 生成 lmports 代码块 -> 生成 Vars 全局变量代码块 -> 生成 Consts 常量代码块 -> 生成 Body 代码块。最后,调用 assernbler. AssernbleFile 函数,将生成的代码块信息写入 zz_generated .deepcopy.go 文件。
在 ExecutePackage 函数中有两个非常重要的函数:
- executeBody
- AssembleFile
executeBody 函数的作用是生成 Body 代码块,通过调用 Generator 接口的 GenerateType 方法实现。对于 deepcopy-gen 代码生成器,定义了自己的 GenerateType 方法,GenerateType 方法生成了三个函数,分别是 DeepCopyInto、DeepCopy、DeepCopyObject。下面就来看一下 executeBody 和 GenerateType 函数的源码分析:(executeBody 函数位于 k8s.io\gengo\generator\execute.go,GenerateType 函数位于 k8s.io\gengo\examples\deepcopy-gen\generators\deepcopy.go)
GenerateType 函数, 其根据传入的类型生成 Bod y 代码块信息。内部通过 Go 语言标准库 text/template 模板语言渲染出生成的 Body 代码块信息。
generator.NewSnippetWriter 内部封装了 text/template 模板语言, 通过将模板应用于数据结构来执行模板。SnippetWriter 对象在实例化时传入模板指令的标识符(即指令开始为$ ,指令结束为$, 有时候也会使用{{}}作为模板指令的标识符) 。例如:
sw.Do(“func (in $.type|raw$) DeepCopy() $.type|raw$ {\n”, args)
SnippetWriter 通过 Do 函数加载模板字符串,并执行渲染模板。模板指令中的点(“.”)表示引用 args 参数传递到模板指令中。模板指令中的(“|”)表示管道符, 即把左边的值传递给右边。
AssembleFile 函数主要作用就是落盘,将已经格式化好的内容写入到文件中,源码解析如下:(源码位于:k8s.io\gengo\generator\execute.go)
4. 总结
使用 gengo 生成代码的不同代码生成器,特定的代码生成器需要覆写特定的方法。整个代码生成的过程为:
- Gather The lnfo : 收集Go 语言源码文件信息及内容。(go/build)
- 获取到每个 Package 中的所有 go 文件及其内容
- Lcxer/Parser : 通过Lexer 词法分析器进行一系列词法分析。(go/token)
- 将每个 Go 文件内容都转换成 Token 序列
- AST Generatol : 生成抽象语法树。(go/ast)
- 根据 Token 序列,将每个 go 文件转成 AST 抽象语法树
- Type C hecker : 对抽象语法树进行类型检查。(go/types)
- 遍历 AST 抽象语法树的每一个节点,进行类型检查,将检查的结果保存起来
- Code Generation : 生成代码,将抽象语法树转换为机器代码。
- 根据类型检查结果,首先找到标有 Tags 的所有类型
- 格式化各个部分的代码,Header、Import、Var、Const、Body
- 写入磁盘文件
评论