注解的作用
提到注解,需要短暂的说明其前世今生。在注解兴起之前,各个框架为了灵活性,基本都是基于 XML/JSON/YAML 之类的配置文件来做模块间的解耦。
因为配置文件可以理解为代码对外的一种特殊的接口,需要先进行设计、代码实现,然后才能对外使用。所以,一般而言配置文件对解耦可以做的比较彻底,但是开发维护成本会比较高。为了解决这个问题,注解这种方式就没提出来了,相对于配置文件它在耦合性上做了一定的让步,换来了更改的易维护性。
例如,著名的 Java 就在 Java 5 中引入了注解,支持对源码中的类、方法、变量、参数和包进行注解,虚拟机通过反射技术,可以在运行时获取到注解内容,并将其相关功能动态加入到目标程序的字节码中。例如,下面就是维基百科中给出的一个 Java 中的注解的例子:
//等同于 @Edible(value = true)
@Edible(true)
Item item = new Carrot();
public @interface Edible {
boolean value() default false;
}
@Author(first = "Oompah", last = "Loompah")
Book book = new Book();
public @interface Author {
String first();
String last();
}
通常注解会被用于:格式检查、减少配置文件试用、减少重复工作,常见于各类框架如 Junit、xUtils 等等。
一些实现注解的开源 Golang 工程
由于注解有其独特的作用,因此,虽然至今(版本<=1.13)Golang 原生版本不支持注解功能,依然有不少的开源项目基于自己的需求实现了注解,其中比较著名的有:
beego中的注解路由实现:https://beego.me/docs/mvc/controller/router.md
Golang 中实现注解的基本思路
参考:https://github.com/MarcGrol/golangAnnotations/wiki
第一步:源码词法分析
Golang 在编译时候涉及到的词法分析和语法分析,其大致过程如下:
Scanner(扫描器)将源代码转换为一系列的 token,以供 Parser 使用。
Parser(语法分析器)将这些 token 转换为 AST(Abstract Syntax Tree, 抽象语法树),以供代码生成。
将 AST 转换为机器码。
这些相关功能的核心代码在了标准库 go/ast 中,可以直接使用。
例如,我们可以通过 github 中的开源工具 goast-viewer(https://yuroyoro.github.io/goast-viewer/index.html) 快速分析以下代码段:
package main
import (
"fmt"
)
// main entry
func main() {
fmt.Printf("Hello, Golang\n")
}
通过 go ast 工具就可以被构建出如下的抽象语法树:
0 *ast.File {
1 . Doc: nil
2 . Package: foo:1:1
3 . Name: *ast.Ident {
4 . . NamePos: foo:1:9
5 . . Name: "main"
6 . . Obj: nil
7 . }
8 . Decls: []ast.Decl (len = 2) {
9 . . 0: *ast.GenDecl {
10 . . . Doc: nil
11 . . . TokPos: foo:3:1
12 . . . Tok: import
13 . . . Lparen: foo:3:8
14 . . . Specs: []ast.Spec (len = 1) {
15 . . . . 0: *ast.ImportSpec {
16 . . . . . Doc: nil
17 . . . . . Name: nil
18 . . . . . Path: *ast.BasicLit {
19 . . . . . . ValuePos: foo:4:2
20 . . . . . . Kind: STRING
21 . . . . . . Value: "\"fmt\""
22 . . . . . }
23 . . . . . Comment: nil
24 . . . . . EndPos: -
25 . . . . }
26 . . . }
27 . . . Rparen: foo:5:1
28 . . }
29 . . 1: *ast.FuncDecl {
30 . . . Doc: *ast.CommentGroup {
31 . . . . List: []*ast.Comment (len = 1) {
32 . . . . . 0: *ast.Comment {
33 . . . . . . Slash: foo:7:1
34 . . . . . . Text: "// main entry"
35 . . . . . }
36 . . . . }
37 . . . }
38 . . . Recv: nil
39 . . . Name: *ast.Ident {
40 . . . . NamePos: foo:8:6
41 . . . . Name: "main"
42 . . . . Obj: *ast.Object {
43 . . . . . Kind: func
44 . . . . . Name: "main"
45 . . . . . Decl: *(obj @ 29)
46 . . . . . Data: nil
47 . . . . . Type: nil
48 . . . . }
49 . . . }
50 . . . Type: *ast.FuncType {
51 . . . . Func: foo:8:1
52 . . . . Params: *ast.FieldList {
53 . . . . . Opening: foo:8:10
54 . . . . . List: nil
55 . . . . . Closing: foo:8:11
56 . . . . }
57 . . . . Results: nil
58 . . . }
59 . . . Body: *ast.BlockStmt {
60 . . . . Lbrace: foo:8:13
61 . . . . List: []ast.Stmt (len = 1) {
62 . . . . . 0: *ast.ExprStmt {
63 . . . . . . X: *ast.CallExpr {
64 . . . . . . . Fun: *ast.SelectorExpr {
65 . . . . . . . . X: *ast.Ident {
66 . . . . . . . . . NamePos: foo:9:2
67 . . . . . . . . . Name: "fmt"
68 . . . . . . . . . Obj: nil
69 . . . . . . . . }
70 . . . . . . . . Sel: *ast.Ident {
71 . . . . . . . . . NamePos: foo:9:6
72 . . . . . . . . . Name: "Printf"
73 . . . . . . . . . Obj: nil
74 . . . . . . . . }
75 . . . . . . . }
76 . . . . . . . Lparen: foo:9:12
77 . . . . . . . Args: []ast.Expr (len = 1) {
78 . . . . . . . . 0: *ast.BasicLit {
79 . . . . . . . . . ValuePos: foo:9:13
80 . . . . . . . . . Kind: STRING
81 . . . . . . . . . Value: "\"Hello, Golang\\n\""
82 . . . . . . . . }
83 . . . . . . . }
84 . . . . . . . Ellipsis: -
85 . . . . . . . Rparen: foo:9:30
86 . . . . . . }
87 . . . . . }
88 . . . . }
89 . . . . Rbrace: foo:10:1
90 . . . }
91 . . }
92 . }
93 . Scope: *ast.Scope {
94 . . Outer: nil
95 . . Objects: map[string]*ast.Object (len = 1) {
96 . . . "main": *(obj @ 42)
97 . . }
98 . }
99 . Imports: []*ast.ImportSpec (len = 1) {
100 . . 0: *(obj @ 15)
101 . }
102 . Unresolved: []*ast.Ident (len = 1) {
103 . . 0: *(obj @ 65)
104 . }
105 . Comments: []*ast.CommentGroup (len = 1) {
106 . . 0: *(obj @ 30)
107 . }
108 }
例如通过这个工具我们就可以看到我们可以分析出每一段注释所对应的,注释内容以及位置:
29 . . 1: *ast.FuncDecl {
30 . . . Doc: *ast.CommentGroup {
31 . . . . List: []*ast.Comment (len = 1) {
32 . . . . . 0: *ast.Comment {
33 . . . . . . Slash: foo:7:1
34 . . . . . . Text: "// main entry"
35 . . . . . }
36 . . . . }
37 . . . }
38 . . . Recv: nil
39 . . . Name: *ast.Ident {
40 . . . . NamePos: foo:8:6
41 . . . . Name: "main"
42 . . . . Obj: *ast.Object {
43 . . . . . Kind: func
44 . . . . . Name: "main"
45 . . . . . Decl: *(obj @ 29)
46 . . . . . Data: nil
47 . . . . . Type: nil
48 . . . . }
49 . . . }
具体的细节可以仔细阅读这篇文章的解释:Go 程序到机器码的编译之旅Go 程序到机器码的编译之旅
第二步:代码生成
当我们通过代码分析找到需要生成的代码之后,可以考虑将代码按照类似的方式进行存储:
Structs: []model.Struct{
{
DocLines: []string{""},
Name: "",
Operations: []model.Operation{
{
DocLines: []string{""},
Name: "",
InputArgs: []model.Field{
{ Name: "", TypeName: "" },
{ Name: "", TypeName: "" },
{ Name: "", TypeName: "" },
{ Name: "", TypeName: "" },
},
OutputArgs: []model.Field{
{ Name: "", TypeName: "" },
},
},
},
},
}
之后,再按照意图基于模块进行中间代码自动生成,这里可以直接借助 golang 中 text/template 包的模板渲染能力进行:
具体使用方式可以参考官方文档:https://golang.org/pkg/text/template/
第三步:自动执行
为了将上述步骤自动化,我们需要借助 golang 提供的另外一个工具: go generate:
go generate 命令是 Golang 1.4 版本引入的一个新命令,当运行 go generate时,它将扫描与当前包相关的源代码文件,找出所有包含”//go:generate”的特殊注释,提取并执行该特殊注释后面的命令,命令为可执行程序,形同shell下面执行。
在我们的需求中,我们在需要处理的源码 package 中增加 “//go:generate”相关命令,作用于仅为当前 package,该命令仅检查当前 package 中是否存在有满足定义的注解,如果有就会进行处理,如果没有则不会改变原有源码内容。
需要注意的是:
“//go:generate” 特殊注释必须在.go源码文件中,且仅当显示运行 go generate 命令时,才会执行特殊注释后面的命令。
命令串行执行的,如果出错,就终止后面的执行。
更多关于 go generate 的资料可以参考官方材料:https://blog.golang.org/generate
番外:Golang 中一种代替注解的方案
参考:https://mritd.me/2018/10/23/golang-code-plugin
“基础代码不变,后续使用者可以将自己的实际需求的需求以热插拔的形式注入进来,Caddy 框架提供了一种解决思路。
// RegisterPlugin plugs in plugin. All plugins should register
// themselves, even if they do not perform an action associated
// with a directive. It is important for the process to know
// which plugins are available.
//
// The plugin MUST have a name: lower case and one word.
// If this plugin has an action, it must be the name of
// the directive that invokes it. A name is always required
// and must be unique for the server type.
func RegisterPlugin(name string, plugin Plugin) {
if name == "" {
panic("plugin must have a name")
}
if _, ok := plugins[plugin.ServerType]; !ok {
plugins[plugin.ServerType] = make(map[string]Plugin)
}
if _, dup := plugins[plugin.ServerType][name]; dup {
panic("plugin named " + name + " already registered for server type " + plugin.ServerType)
}
plugins[plugin.ServerType][name] = plugin
}
套路就是定义一个 map,map 里用于存放一种特定形式的 func,并且暴露出一个方法用于向 map 内添加指定 func,然后在合适的时机遍历这个 map,并执行其中的 func。这种套路利用了 Go 函数式编程的特性,将行为先存储在容器中,然后后续再去调用这些行为