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 的代码生成逻辑与编译器原理非常类似,大致可分为如下几个过程:

  1. Gather The lnfo : 收集Go 语言源码文件信息及内容。(go/build)
  2. Lcxer/Parser : 通过Lexer 词法分析器进行一系列词法分析。(go/token)
  3. AST Generatol : 生成抽象语法树。(go/ast)
  4. Type C hecker : 对抽象语法树进行类型检查。(go/types)
  5. Code Generation : 生成代码,将抽象语法树转换为机器代码。

2. gengo入口流程分析

在 gengo 项目中,examples 目录中给了一些自定生成代码的使用实例,例如 deepcopy-gen、defaulter-gen 等。现在以 deepcopy-gen 生成器为例,查看 gengo 的流程,下面过程是在 k8s.io\gengo\examples\deepcopy-gen\main.go 文件中:

  1. 初始化 GeneratorArgs。GeneratorArgs 结构体表示代码生成器的一些参数,例如需要解析的源文件、生成文件的版权信息等等。
  2. 生成自定义参数 CustomArgs,并且将自定义参数 CustomArgs 赋值到 GeneratorArgs 参数集。CustomArgs.BoundingDirs 表示只处理这些目录下的根类型。
  3. 执行 Execute() 方法,代码生成器的入口函数。Execute() 是 GeneratorArgs 结构体的方法。

k8s.io\gengo\examples\deepcopy-gen\main.go 文件源代码如下:

Copy to Clipboard

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):

Copy to Clipboard

初始化并且设置了自定义参数后,GeneratorArgs 参数就设置好了,这时候,每个代码生成器都会通过 Packages 函数将具体生成器的 GeneratorArgs  对象中的 Tags 修改成对应和生成器的 Tags。最后执行 GeneratorArgs 结构体的 Execute 方法,正式进入代码生成器的核心逻辑。

3.1.2 收集Go 包信息并读取源码内容

上面我们介绍了 GeneratorArgs 结构体的初始化,下面我们来看一下入口程序 Execute 方法都做了哪些操作。这里先不介绍传入的参数,先大致看一下 Execute 方法的主要功能:

  1. 如果开启了命令行模式,那么就添加命令行选项(3.1.2)
  2. 创建代码生成器,并且初始化(3.1.2)
  3. 为改代码生成器创建一个全局的 Context 上下文环境(3.3.3)
  4. 生成 Packages 列表(3.3.4)
  5. 根据每一个 Package 解析并且生成最终的代码(3.3.5)

Execute 函数源代码如下,代码路径为:k8s.io\gengo\args\args.go 。

Copy to Clipboard

第一步,添加命令行选项。这里用的 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,源码如下:

Copy to Clipboard

Execute 函数的第二步,创建代码生成器,并且初始化。GeneratorArgs 结构体的 NewBuilder 方法来创建代码生成器。

下面看一下 b, err := g.NewBuilder()  这里的 NewBuilder 函数,其主要作用就是收集 Go 包信息并读取源码内容。步骤为:

  1. 通过 go/build package 生成代码生成器,用来收集 Go Package 的信息。
  2. 将参数列表 GeneratorArgs 结构体中的 GeneratedBuildTag 字段,添加到 go/build 的 Context 中。
  3. 代码生成器通过 –input-dirs 参数指定传入的 G0 包路径,通过 build.Import 方法收集  G0 包的信息。这里 AddDir() 和 AddDirRecursive() 两个函数的主要作用是一样的,都是通过 build.Import 方法来获取 Go Package 的详细信息,唯一的区别就是 AddDirRecursive 函数是可以递归将其子目录也进行遍历。

NewBuilder 函数的源码如下,代码路径为:k8s.io\gengo\args\args.go。

Copy to Clipboard

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  各个字段初始化后返回。

Copy to Clipboard

接下来看一下 AddDir 和 AddDirRecursive 的区别。两个函数的功能是一样的,扫描传入参数的文件夹,获取所有 .go 文件的信息。传入参数 dir 应该是一个 package 的目录,如果这个目录下不是一个 package 会搜索 GOROOT、GOPATH 和 which go 的位置目录。

AddDirRecursive 函数相比于 AddDir 唯一区别是会递归搜索子目录,实现很简单,就是利用 filepath.Walk 函数,进行遍历每个子目录。代码目录为:k8s.io\gengo\parser\parse.go

Copy to Clipboard

无论是 AddDir 函数还是 AddDirRecursive 函数,都调用了 importPackage 函数,该函数的调用过程为:

  • importPackage 函数,功能:如果当前路径的包没有获取 Package 信息,那么就先调用 addDir 函数,获取 Package 信息,并生成 AST 语法树,然后执行类型检查 typeCheckPackage 如果当前路径已经获取了 Package 信息,那么就直接进行类型检查
    • addDir 函数,功能:生成 Package 信息,并且生成 AST 语法树
      • importBuildPackage 函数,功能:利用 go/build Context.Import 函数,生成 Package 信息,并且添加到 Builder 对象中
      • addFile 函数,功能:读取每个 Go 文件内容,将其内容转成 AST 语法树,下一节介绍
    • typeCheckPackage 函数,功能:类型检查

下面为 获取 Go Package 信息的几个函数的源码,代码目录为:k8s.io\gengo\parser\parse.go 。

Copy to Clipboard

在获取 Go Package 信息时,又调用了 addDir 函数,下面就是 addDir 函数源码分析。

Copy to Clipboard

这里有两个主要函数,分别是 importBuildPackage 和 addFile。addFile 函数作用是 go 文件的解析,下一节介绍,这里先看 importBuildPackage 函数作用。

Copy to Clipboard

importWithMode 函数是对 go/build 的最后一层封装,最终调用了 Import 函数获取到 Package 信息。

Copy to Clipboard

这里最终调用了 go/build Context.Import 函数,获取当前目录的 Package 信息。

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 词法分析器。

  1. 首先,通过 token.NewFileSet 实例化得到 token.FileSet 对象,该对象用于记录文件中的偏移量、类型、原始字面盘及词法分析的数据结构和方法等,Lexer 词法分析器会最终生成 Token 序列数据。
  2. 得到 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。源码如下:

Copy to Clipboard

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。

Copy to Clipboard

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 结构体,该结构体可以表示任何类型结构。源码如下:

Copy to Clipboard

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。

Copy to Clipboard

NewContext 的作用主要有两个:

  • 通过 FindTypes 方法,获取所有类型的 Package 的信息
  • 创建一个全局的 Context 对象,并且赋值

源码如下,代码路径为: k8s.io\gengo\generator\generator.go

Copy to Clipboard

首先分析 FindTypes 方法,该方法的主要作用就是 先求出所有已经解析好的 package 列表,然后遍历这些列表,将用户请求的 package 存入到 Universe 对象中。源码如下,代码路径为:k8s.io\gengo\parser\parse.go。

Copy to Clipboard

在 FindTypes 函数中,遍历每一个 package 调用了 findTypesIn 函数来具体操作,findTypesIn 函数的作用是

  • 通过 parsedfile 路径,找到通过类型检查后生成的 types 的 Package 对象。
  • 如果 Universe 对象中有该 Package ,就更新,如果没有,就创建一个 Package 到 Universe  对象中
Copy to Clipboard

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 代码生成器为例):
    1. 先取出文件开头的注释信息,并且解析,将 “// +k8s:deepcopy-gen=xxx” 注释信息解析为 enabledTagValue 结构体对象
    2. 根据 = 后面的值进行判定,如果 = 后面的值为 “package” ,表示当前package 需要进行代码生成,pkgNeedsGeneration 设置为 true。并且判断源路径中是否有 /vendor/ 路径,如果有,将 path 设置为带有 /vendor 的路径。然后,将 结果放入 generator.Packages 中。
    3. 如果 = 后面的值不是 “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 ,源码如下:

generators.Packages 的源码为:

Copy to Clipboard

该函数中,有几个重要的函数:

  1. arguments.LoadGoBoilerplate() – 加载注释信息
  2. extractEnabledTag() – 将文件开头的注释信息进行解析,解析出 // +k8s:deepcopy-gen=xxx 的 Tags 信息
  3. extractEnabledTypeTag() – 对某个类型的注释信息进行解析,同样解析出 // +k8s:deepcopy-gen=xxx 的 Tags 信息

三个函数的额源码分析如下:

LoadGoBoilerplate() 函数加载通过 –go-header-file 选项传入的注释信息文件,源码路径为 k8s.io\gengo\args\args.go 。源码如下:

Copy to Clipboard

extractEnabledTag 函数就是将关键字 package 上的注释信息进行解析,将带有 // +k8s:deepcopy-gen=xxx 的解析后返回。源码如下:

Copy to Clipboard

extractEnabledTypeTag 函数就是将 某个类型的注释信息取出,在调用 extractEnabledTag 函数对类型声明的注释信息进行解析。源码路径为: k8s.io\gengo\examples\deepcopy-gen\generators\deepcopy.go

Copy to Clipboard

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.deepcop
  • 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 文件的源码分析:

DefaultPackage 结构体,就是默认的需要代码生成的 Package 信息,generator.Package 接口定义的 6 个方法。

Copy to Clipboard

DefaultGen 是默认的代码生成器,在代码生成阶段使用。

Copy to Clipboard

在 3.3.4 实例化 generator.Packages 对象时,deepcopy- gen 代码生成器根据输入的包的目录路径〈即输入源) ,实例化 generator.Packages 对象,根据 generator. Packages 结构生成代码,代码示例如下:  代码路径 k8s.io\gengo\examples\deepcopy-gen\generators\deepcopy.go。

Copy to Clipboard

在 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)

逐步分析代码生成的过程:

  1. 调用 ExecutePackages 函数
  2. 遍历 packages,调用具体的 ExecutePackage 函数进行代码生成
  3. 调用 Generator 接口的各个方法,生成各个部分的代码片段,保存在 generator.File 结构体中:
    • PackageVars – Generator 接口的方法,生成全局变量代码块信息,当前 deepcoy-gen 代码生成器未使用 Vars .
    • PackageConsts – Generator 接口的方法,生成代码块信息, 当前 deepcoy-gen 代码生成器未使用 Consts .
    • Imports – Generator 接口的方法,生成 import 代码块,引入外部包
    • Header – 码块信息,包括 build tag 和 license boilerplate 文件(存放开源软件作者及开源协议等信息) 其中 license boilerplat文件可以从 hacklboilerplate/boilerplate.go.txt 中获取.
    • Body – Body 代码块信息,生成 DeepCopy 深复制函数
  4. 调用 AssembleFile 函数,将 generator.File 结构体中的代码片段,写入到文件中。

注意:deepcopy-gen 代码生成器没有生成 Consts 和 Vars 的代码片段,因此,deepcopy-gen 代码生成器就没有覆盖默认代码生成器的 PackageVars 、PackageConsts 方法,而是重写了 Imports 、GenerateType 方法用来自定义生成 import 代码段和 Body 代码段。

下面就来看一下具体代码生成过程的源码分析,也就是 Execute 函数的最后一步:、

代码路径在:k8s.io\gengo\generator\execute.go。该函数就是执行生成代码的入口函数,函数的接收参数是输出路径和需要生成代码的 Packages 列表。

该函数的功能:遍历所有的 Packages 列表,每一个 Package 调用 ExecutePackage 函数执行代码生成的逻辑。源码如下:

Copy to Clipboard

ExecutePackage 函数位于 k8s.io\gengo\generator\execute.go 文件中。该函数的作用就是先生成代码片段,保存在 generator.File 结构体对象中,然后调用 AssembleFile 函数,将 generator.File 结构体对象中的内容写入到磁盘文件中。源码如下:

Copy to Clipboard

ExecutePackage 代码生成执行流程· 生成 Header 代码块 -> 生成 lmports 代码块 -> 生成 Vars 全局变量代码块 -> 生成 Consts 常量代码块 -> 生成 Body 代码块。最后,调用 assernbler. AssernbleFile 函数,将生成的代码块信息写入 zz_generated .deepcopy.go 文件。

在 ExecutePackage 函数中有两个非常重要的函数:

  • executeBody
  • AssembleFile

4. 总结

使用 gengo 生成代码的不同代码生成器,特定的代码生成器需要覆写特定的方法。整个代码生成的过程为:

  1. Gather The lnfo : 收集Go 语言源码文件信息及内容。(go/build)
    • 获取到每个 Package 中的所有 go 文件及其内容
  2. Lcxer/Parser : 通过Lexer 词法分析器进行一系列词法分析。(go/token)
    • 将每个 Go 文件内容都转换成 Token 序列
  3. AST Generatol : 生成抽象语法树。(go/ast)
    • 根据 Token 序列,将每个 go 文件转成 AST 抽象语法树
  4. Type C hecker : 对抽象语法树进行类型检查。(go/types)
    • 遍历 AST 抽象语法树的每一个节点,进行类型检查,将检查的结果保存起来
  5. Code Generation : 生成代码,将抽象语法树转换为机器代码。
    • 根据类型检查结果,首先找到标有 Tags 的所有类型
    • 格式化各个部分的代码,Header、Import、Var、Const、Body
    • 写入磁盘文件