一、什么是 AST?
AST,即抽象语法树。它是源代码语法结构的一 种抽象表示。 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
并发、反射和 AST 三者是构建复杂系统(中间 件)的基石,真正的高级工具。
二、AST能干什么呢?
- 精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作
- 代码生成:Go 因为本身没有动态代理之类的工具,所以代 码生成大行其道,非常流行
开源例子:
- GORM Gen 子项目 github.com/go-gorm/gen
- 依赖注入 Google Wire github.com/google/w
三、AST 编程入门
package ast
import (
"fmt"
"go/ast"
"reflect"
)
type printVisitor struct {
}
func (t *printVisitor) Visit(node ast.Node) (w ast.Visitor) {
if node == nil {
fmt.Println(nil)
return t
}
val := reflect.ValueOf(node)
typ := reflect.TypeOf(node)
for typ.Kind() == reflect.Ptr {
typ = typ.Elem()
val = val.Elem()
}
fmt.Printf("val: %+v, type: %s \n", val.Interface(), typ.Name())
return t
}
func TestAst(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", "", parser.ParseComments)
if err != nil {
t.Fatal(err)
}
ast.Walk(&printVisitor{}, f)
}
解析出来的 AST 树看上去可能不够优雅,AST 编程基本上都是一边打断点一边写,因为很难手动画出目标 AST 树。
3.1 基本步骤
- 使用
token.NewFileSet来创建FileSet(逻辑上的一种文件集合, 逻辑就意味着,其实它里面的文件可以不必真的放在磁盘上,而是手动添加进去) - 使用
parser.ParseFile开始解析源码,可以指定 mode - 实现
ast.Visitor的接口,作为遍历 AST 的处理 器,我们可以利用该实现达到读取、篡改 AST 树的目的 - 调用
ast.Walk开始遍历 AST
func TestAst(t *testing.T) {
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", "", parser.ParseComments)
if err != nil {
t.Fatal(err)
}
ast.Walk(&printVisitor{}, f)
}
3.2 Visitor 设计模式详解
Visitor 设计模式:这种设计模式常常用于遍历复杂的数据结构,典型的就是树形结构, 一些情况下可以用于遍历链表等结构。
- 对于 Node 来说,就是接收 Visitor,然后具体的实现里面再调用 Visitor 的 Visit 方法,并且将自身传入下去。
- 对于 Visitor 来说, 一般是要做类型判断,判 断该 Node 的实际类型,自己究竟能不能处理。
3.3 ast.Visitor 接口
Go 的 AST 包暴露了 Visitor 接口,用于 遍历整颗 AST 树, ast.Visitor 和普通的 Visitor 比起来,它 的返回值也是一个 Visitor,这个 Visitor 被用来进一步遍历剩下的节点。
3.4 ast.Node 接口
ast.Node 接口代表了 AST 的节点。任何一个 Go 语 句在 AST 里面都代表了一个节点,多个语句之间 可以组成一个更加复杂的节点。一个节点由三个关键子类组成:
- 表达式 Expr:例如各种内置类型、方法调用、 数组索引等
- 语句 Stmt:如赋值语句、if 语句等
- 声明 Decl:各种声
这三个关键类不需要记,写代码的时候再去看具体的 实现,因为总共有 69 种实现,Node 接口定义的方法其实不太常用,很多时候操 作 AST 都不关心 Pos,也就是位置。
3.4.1 GenDecl
GenDecl 代表通用声明,一般是:
- 类型声明,以 type 开头的
- 变量声明,以 var 开头的
- 常量声明,以 const 开头的
- import 声明,以 import 开头的
基本上,都要进一步判断 Tok 来判断究竟是什么声明。
3.4.2 FuncDecl
FuncDecl 是方法声明:
- Doc 是文档。注意,不是所有的注释都是文档,要符合 Go 规范的文档
- Recv 接收器
- Name 方法名
- Type 方法签名:里面包含泛型参数、参数类型、返回值
3.4.3 StructType
- StructType 代表了一个结构体声明了Fields:字段。实际上,定义在该结构体上的方法是放在 ast.File 的 Decls 里
3.4.4 内置结构
- 内置结构都有对应的 Node 实现。
3.5 开源实例
(1) ast.Walk 实现
例如方法声明,依次遍历它的文档、接收器、名字、参数类型、返回值类型和方法体。
3.6 代码演示
(1) 自定义注解
在 Java 里面有一种机制叫做注解,可以看做是特殊的接口。然而在Go 里面并没有,不过我们可以通过 AST 来设计一种全新的注解。这些注解将会被用于代码生成,可以看做是一种辅助的声明元数据的方式。
所谓注解(注释):
- 单行注解形式:// @annotation_name annotation_value
- 多行注解形式:/* @annotation_name annotation_value */
演示如何获得类型定义、方法声明、字段上的注解。
(2) 篡改源码 背景:
- 一般为了 DEBUG,我们会考虑在方法调用之前先把请求输出到日志。
- 一般的做法是手动写代码,但是我们可以考虑借助 AST 来完成。
package ast
import (
"fmt"
)
func Hello(name string) string {
return "hello, " + name
}
func Hello1(firstName, lastName string, age int) string {
return fmt.Sprintf("hello, %s %s, %d", firstName, lastName, age)
}
关键步骤:
- 加入 import
- 加入 log 语句
3.7 面试要点
AST 在面试中大多数的情况下都是和编译原理一起面的。不过因为编译原理实在太难,大部分面试官根本不会面编译原理,所以只有在你面技术专家,或者编译相关岗位的时候才会面得比较多。
- 什么是 AST(抽象语法树)?是源代码的一种树形结构的表示,我们可以通过修改 AST 来修改源码。
- AST 编程有什么用?语法检查、格式检查、错误检查、自动补全、优化代码、代码生成,基本上就是只要你够强,用 AST 你就可以为所欲为,甚至设计新的语言。
- Go 有没有注解?没有,但是我们可以用 AST 来自己设计。
- 什么是 Visitor 设计模式?Visitor 模式主要用于遍历复杂结构体,要点在于接收一个节点作为输入,并且返回一个遍历子节点的 Visitor。