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 代码导读

  1. args 包
    1. 定义了生成代码的工具的常见输入参数,比如 InputDirs, OutputBase, OutputPackagePath 等等
    2. 解析参数的辅助函数 - 使用 pflag 解析参数; LoadGoBoilerplate;
    3. 制造出 parser.Builder
    4. Execute 入口:implements main(),执行
      1. Parse 参数
      2. parser.NewBuilder
      3. generator.NewContext
      4. context.ExecutePackages(g.OutputBase, packages);: context 包装 =》 builder 包装 =》 来源数据,参数
  2. parser 包: 解析输入文件 使用 go/build 包
  3. types 包
    1. comments: ExtractCommentTags 从 lines 里面提取 +key=value 风格的 comment
    2. flatten
    3. 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里面的元素
  4. namer 包
    1. ImportTracker passed to a namer.RawNamer, to track the imports needed for the types it names.
  5. generator 包:
    1. SnippetWriter:是对 golang 自带对template 包的简单封装,增加了 namer里面的函数
    2. import_tracker: 返回 namer.ImportTracker
    3. generator:gengo 依次执行, 这是一个 interface,实际实现的插件要实现这个 interface
      1. Filter() :这个插件是否关系当前的类型,如果不关心,下面的流程都不执行
      2. Namers() // Subsequent calls see the namers provided by this.
      3. PackageVars() var (...)
      4. PackageConsts() const xxx
      5. Init() 初始化方法 func init(){}
      6. GenerateType() // Called N times, once per type in the context's Order.
      7. Imports() import (name "path/to/pkg")
    4. Context: Context is global context for individual generators to consume. 所有的上下问信息都有了
      1. Namers
      2. Universe: 所有的类型
      3. incomingImports
      4. Inputs
      5. builder
    5. execute 真正的执行,是Context的函数
      1. 核心是 (c *Context) ExecutePackage(outDir string, p Package) 函数,会依次执行 generator interface里面的方法
      2. 其中文件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 框架中的核心 面向切面AOPIOC控制反转 都是基于 Annotation 实现的。

由于类似的概念实在是太好用了,在 go 语言中,很多先行者也做了一些尝试,比如针对 IOC 的 facebook injectuber diggoogle wirego-spring, 其中 inject、 dig 和 go-spring 都是基于 reflect 的,受制于 golang 的反射能力,代码中并不能做到像 Java 中那么智能,注入之前还是需要先手动提供一些构建方法,不是那么方便,wire 基于代码生成,风格和 Java 中差别比较大。

那么我们能不能利用 gengo 实现一套 annotation 系统,实现类似 Java 中的注解功能呢,如果实现了这个,那么 用它来实现 IOC 只是其中的一个用例插件。

Go-Annotation

实战代码在 go-annotation

对照 Java 的 Annotation 系统,一个 Annotation 比较关注的两个点:

  1. Retention:是 runtime 还是仅仅是 编译时使用,runtime 就忽略了,这点 golang 可以只关注 runtime 类型,也就是所有的 annotation 信息都会在 运行时暴露,以简化设计
  2. Target:注解使用的 对象范围是什么 是 类型、字段、方法、参数、还是本地变量、包 ?对于 golang 而言,最紧缺的能力在于 类型 和 方法的注解,字段的注解因为 golang 的提供 tag 能力结合 reflect 包,可以解决大部分问题。所以第一个版本,我们只关注 target 为 type、package、method 的三种类型。

Annotation 系统具体设计

使用 Annotation@Annotation名字=AnnotationBody 表示使用一个具体的 annotation, Annotation 是一个固定前缀,可以作为工具的输入参数修改,@ 后为 Annotation的名字,为一个具体的 Annotation类型,AnnotationBody 是注解的具体内容,为了简化设计,我们定义 AnnotationBody 为 JSON 格式,具体的注解内容会被当成 JSON 文本,再具体 解析到一个 Annotation 类型中去。

  1. 注解的注册,这点可以在代码中生成,同时结合 lib 包完成
  2. 注解自定义的 代码生成,这点有 注解插件 的 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 的时候

  1. 能够自动创建 字段 B1,B2
  2. 自动创建的 ComponentB 是一个 Singleton 类型,因此我们希望 B1字段 和 B2字段应该一样,也就是说 ComponentB 的实例只会创建一个。
  3. 自动创建 ComponentB 的时候能够自动创建 ComponentC,由于 ComponentC 有一个无函数的 NewComponentC 函数,我们认为 这是一个 Constructor 函数,因此创建时应该使用NewComponentC 函数创建 ComponentC
  4. 自动创建 ComponentC 后,由于字段 D 也是 autowired 的,我们希望自动识别出 NewComponentD 函数为 Constructor 函数,然后自动创建 ComponentD

例如, 用 Annotation 系统实现的内置插件 Component, 实现了类似 Java 中的依赖注入功能, 具体使用请参考 examples/example_test.go

差不多了,这基本上是一个可以使用的 并且实现了 内置 IOC 插件的 Annotation 系统了,当然这才是个开始,很多好用的插件还可以继续实现。

欢迎关注这个项目的进展 go-annotation

参考

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
Golang Annotation 系统 - Gengo 实战
代码生成的技术在各种语言中都很常用,尤其是静态语言,利用代码生成的技术可以实现一些大幅提高生产效率的工具。
<<上一篇
下一篇>>