本文介绍 gengo 工具的 golang 代码生成技术,以及基于此完成的 golang annotation 插件。
背景代码生成的技术在各种语言中都很常用,尤其是静态语言,利用代码生成的技术可以实现一些大幅提高生产效率的工具。
比如 Java 中的 Annotation Lombok 会在Javac 解析成抽象语法树之后(AST), Lombok 根据自己的注解处理器,动态的修改 AST,增加新的节点(所谓代码),最终通过分析和生成字节码,根据具体的 Annotation 生成 Class 的 Getter、Setter 方法等,降低开发者的工作量。
广义上讲 C++ 的模版方法也是类似的代码生成技术,有时候使用生成代码技术不仅仅是出于降低工程量的角度,由于避免了运行时的自省调用,利用代码生成完成的功能往往执行效率也更好,比如 ffjson。
我们在另一篇文章中介绍的 protobuf 代码生成 也是一种常见的代码生成工具。利用 protoc 的生成工具,可以生成各种语言的代码,rpc server, client 模版代码,配合各种插件还能生成文档、脚本、Http 网关代码等。
代码生成Gengogengo 是 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 生成 DeepCopyDeepCopyInto 等函数代码
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.NewBuildergenerator.NewContextcontext.ExecutePackages(g.OutputBase, packages);: context 包装 =》 builder 包装 =》 来源数据,参数parser 包: 解析输入文件 使用 go/build 包types 包comments: ExtractCommentTags 从 lines 里面提取 +key=value 风格的 commentflattentypes: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.ImportTrackergenerator:gengo 依次执行, 这是一个 interface,实际实现的插件要实现这个 interfaceFilter() :这个插件是否关系当前的类型,如果不关心,下面的流程都不执行Namers() // Subsequent calls see the namers provided by this.PackageVars() var (...)PackageConsts() const xxxInit() 初始化方法 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. 所有的上下问信息都有了NamersUniverse: 所有的类型incomingImportsInputsbuilder 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。
参考k8s代码自动生成过程的解析十分钟搞懂Lombok使用与原理kubernetes-deep-dive-code-generation-customresources ---来自腾讯云社区的---王磊-AI基础
微信扫一扫打赏
支付宝扫一扫打赏