Golang Annotation 系统 - Gengo 实战
本文介绍 gengo 工具的 golang 代码生成技术,以及基于此完成的 golang annotation 插件。
背景
代码生成的技术在各种语言中都很常用,尤其是静态语言,利用代码生成的技术可以实现一些大幅提高生产效率的工具。
比如 Java 中的 Annotation Lombok 会在Javac 解析成抽象语法树之后(AST), Lombok 根据自己的注解处理器,动态的修改 AST,增加新的节点(所谓代码),最终通过分析和生成字节码,根据具体的 Annotation 生成 Class 的 Getter、Setter 方法等,降低开发者的工作量。
广义上讲 C++ 的模版方法也是类似的代码生成技术,有时候使用生成代码技术不仅仅是出于降低工程量的角度,由于避免了运行时的自省调用,利用代码生成完成的功能往往执行效率也更好,比如 ffjson。
我们在另一篇文章中介绍的 protobuf 代码生成 也是一种常见的代码生成工具。利用 protoc 的生成工具,可以生成各种语言的代码,rpc server, client 模版代码,配合各种插件还能生成文档、脚本、Http 网关代码等。
Gengo
gengo 是 kubernetes 项目中常用的代码生成工具,kubernetes 项目中大量使用了这个工具用于代码生成。 gengo 更多的设计为一个比较通用的代码生成工具,完成代码表达树解析,生成的工作。
在 kubernetes 中的使用
code-generator 是对 gengo 的一层包装,完成 kubernetes 中常见的一些代码生成任务,比如 客户端代码生成、deepcopy 类代码生成等等,大部分是围绕 kubernetes api 对象的生成工具。
工具 |
作用 |
---|---|
client-gen |
为 API 资源创建 typed clientsets 即 rest client |
conversion-gen |
用于为 API 资源 生成 |
deepcopy-gen |
为 API 资源 T 生成 DeepCopy\\DeepCopyInto 等函数代码 |
defaulter-gen |
API 资源的 default 函数还是要手写的,这个工具会帮助 注册哦 default 函数,用于自动执行 default 函数 |
informer-gen |
为API 资源创建 informers,它会基于接口提供 event 事件来对服务器上的自定义资源的任何改动做出反应 |
lister-gen |
为API 资源 创建 listers 函数,会提供一个只读的缓存层来相应GET和LIST请求 |
openapi-gen |
为API资源创建 openapi 定义文档 |
set-gen |
为 builtin 类型创建对应的 sets 类,即 hash set 类型,由于 go不支持泛型,利用这个工具自动生成代码 |
原理
Gengo 的目标是完成一个方便用户自行实现各种代码生成工具的库,他完成了几项工作
- 解析代码文件,解析完成的对象为 package、type
- 定义生成文件的工作模板,即 generator interface,开发者只需要简单实现其中的函数,就可以完成解析代码的大部分工作
- 渲染辅助工具,如 importer、namer 分别完成生成代码的 import 语句生成、type 渲染等功能。
gengo 代码导读
- args 包
- 定义了生成代码的工具的常见输入参数,比如 InputDirs, OutputBase, OutputPackagePath 等等
- 解析参数的辅助函数 - 使用 pflag 解析参数; LoadGoBoilerplate;
- 制造出 parser.Builder
- Execute 入口:implements main(),执行
- Parse 参数
- parser.NewBuilder
- generator.NewContext
- context.ExecutePackages(g.OutputBase, packages);: context 包装 =》 builder 包装 =》 来源数据,参数
- parser 包: 解析输入文件 使用 go/build 包
- types 包
- comments: ExtractCommentTags 从 lines 里面提取 +key=value 风格的 comment
- flatten
- types:Package holds package-level information,比如 path,name,comments,type字典,function字典,import字典等; Universe 是 Package 字典,一组 Package;Type 是 a subset of possible go types. Member 是 Type的 memers里面的元素
- namer 包
- ImportTracker passed to a namer.RawNamer, to track the imports needed for the types it names.
- generator 包:
- SnippetWriter:是对 golang 自带对template 包的简单封装,增加了 namer里面的函数
- import_tracker: 返回 namer.ImportTracker
- generator:gengo 依次执行, 这是一个 interface,
实际实现的插件要实现这个 interface
- Filter() :这个插件是否关系当前的类型,如果不关心,下面的流程都不执行
- Namers() // Subsequent calls see the namers provided by this.
- PackageVars()
var (...)
- PackageConsts()
const xxx
- Init() 初始化方法
func init(){}
- GenerateType() // Called N times, once per type in the context's Order.
- Imports()
import (name "path/to/pkg")
- Context: Context is global context for individual generators to consume. 所有的上下问信息都有了
- Namers
- Universe: 所有的类型
- incomingImports
- Inputs
- builder
- execute 真正的执行,是Context的函数
- 核心是
(c *Context) ExecutePackage(outDir string, p Package)
函数,会依次执行 generator interface里面的方法 - 其中文件assemble,format 等交给 DefaultFileType 完成。具体的函数为 importsWrapper/assembleGolangFile
- 核心是
实战
实战目标
使用过 Java 开发项目的同学一定对 java 中的 annotation 系统印象深刻,让我们来看一段代码。
@Getter
@Setter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class SomeEntity implements Serializable {
@CreatedBy
@Column(name = "create_by", updatable = false)
@ApiModelProperty(value = "创建人", hidden = true)
private String createBy;
@CreationTimestamp
@Column(name = "create_time", updatable = false)
@ApiModelProperty(value = "创建时间", hidden = true)
private Timestamp createTime;
}
这段代码中 Annotation 的行数甚至超过的 实际的Java 代码,利用 Annotation 的强大,Java 开发中可以省略大量的重复代码,各种高级库和框架利用 annotation 完成了大量的自动化工作。Spring 框架中的核心 面向切面AOP
、IOC控制反转
都是基于 Annotation 实现的。
由于类似的概念实在是太好用了,在 go 语言中,很多先行者也做了一些尝试,比如针对 IOC 的 facebook inject、uber dig、google wire 、go-spring, 其中 inject、 dig 和 go-spring 都是基于 reflect 的,受制于 golang 的反射能力,代码中并不能做到像 Java 中那么智能,注入之前还是需要先手动提供一些构建方法,不是那么方便,wire 基于代码生成,风格和 Java 中差别比较大。
那么我们能不能利用 gengo 实现一套 annotation 系统,实现类似 Java 中的注解功能呢,如果实现了这个,那么 用它来实现 IOC 只是其中的一个用例插件。
Go-Annotation
实战代码在 go-annotation
对照 Java 的 Annotation 系统,一个 Annotation 比较关注的两个点:
- Retention:是 runtime 还是仅仅是 编译时使用,runtime 就忽略了,这点 golang 可以只关注 runtime 类型,也就是所有的 annotation 信息都会在 运行时暴露,以简化设计
- Target:注解使用的 对象范围是什么 是 类型、字段、方法、参数、还是本地变量、包 ?对于 golang 而言,最紧缺的能力在于 类型 和 方法的注解,字段的注解因为 golang 的提供 tag 能力结合 reflect 包,可以解决大部分问题。所以第一个版本,我们只关注 target 为 type、package、method 的三种类型。
Annotation 系统具体设计
使用 Annotation@Annotation名字=AnnotationBody
表示使用一个具体的 annotation, Annotation 是一个固定前缀,可以作为工具的输入参数修改,@
后为 Annotation的名字,为一个具体的 Annotation类型,AnnotationBody 是注解的具体内容,为了简化设计,我们定义 AnnotationBody 为 JSON 格式,具体的注解内容会被当成 JSON 文本,再具体 解析到一个 Annotation 类型中去。
- 注解的注册,这点可以在代码中生成,同时结合 lib 包完成
- 注解自定义的 代码生成,这点有 注解插件 的
Template() string
函数完成,如果某个注解 实现了Template() string
函数,表示这种注解插件同时需要生成一些自定义的代码。
内置插件 Component 设计
Component 插件实现类似 Java 中的依赖注入能力。比如下面的 定义。
// Annotation@Component
type ComponentA struct {
B1 *ComponentB `autowired:"true"` // Will populate with new(ComponentB)
B2 *ComponentB `autowired:"true"` // Will populate with new(ComponentB)
B3 *ComponentB
}
// Annotation@Component={"type": "Singleton"}
type ComponentB struct {
C *ComponentC `autowired:"true"` // Will populate with NewComponentC()
}
// Annotation@Component
type ComponentC struct {
D *ComponentD `autowired:"true"` // Will populate with NewComponentD()
IntValue int
}
func NewComponentC() *ComponentC {
return &ComponentC{IntValue: 1}
}
// Annotation@Component
type ComponentD struct {
IntValue int
}
func NewComponentD() (*ComponentD, error) {
return &ComponentD{IntValue: 2}, nil
}
我们希望 创建 ComponentA 的时候
- 能够自动创建 字段 B1,B2
- 自动创建的 ComponentB 是一个
Singleton
类型,因此我们希望 B1字段 和 B2字段应该一样,也就是说 ComponentB 的实例只会创建一个。 - 自动创建 ComponentB 的时候能够自动创建 ComponentC,由于 ComponentC 有一个无函数的 NewComponentC 函数,我们认为 这是一个 Constructor 函数,因此创建时应该使用NewComponentC 函数创建 ComponentC
- 自动创建 ComponentC 后,由于字段 D 也是
autowired
的,我们希望自动识别出 NewComponentD 函数为 Constructor 函数,然后自动创建 ComponentD
例如, 用 Annotation 系统实现的内置插件 Component, 实现了类似 Java 中的依赖注入功能, 具体使用请参考 examples/example_test.go
差不多了,这基本上是一个可以使用的 并且实现了 内置 IOC 插件的 Annotation 系统了,当然这才是个开始,很多好用的插件还可以继续实现。
欢迎关注这个项目的进展 go-annotation。