文章目录
一、Golang AST基础
1. 什么是AST语法树
让我们从所需的一些知识开始。 什么是 AST(Abstract Syntax Tree)?根据维基百科:
在计算机科学中,抽象语法树 (AST) 或简称语法树是用编程语言编写的源代码的抽象语法结构的树表示形式。树的每个节点都表示源代码中出现的一个构造。
大多数编译器和解释器使用Abstract Syntax Tree抽象语法树作为源代码的内部表示,实际上是一个解析树(parse tree)的一个精简版本。一棵解析树是包含代码所有语法信息的树型结构,它是代码的直接翻译。所以解析树,也被成为具象语法树(Concret Syntax Tree, 简称CST);而抽象语法树,忽略了一些解析树包含的一些语法信息,剥离掉一些不重要的细节,AST 通常省略语法树中的分号、换行符、空格、大括号、方括号和圆括号等。
主要特点:
– 抽象化:AST 只关心程序的语法结构,而不关心具体的语法(如括号、关键字等)。
– 树形结构:AST 是一棵树,每个节点表示程序中的一个语法元素(如操作符、变量、常量等)。
– 层次化:不同层级的节点表示程序的不同语法层级,根节点通常是程序的起点或最外层表达式。
组成部分:
– 节点:每个节点代表代码中的一个语法成分(如运算符、操作数、变量等)。
– 边:连接节点的边代表语法规则中的层级关系或操作顺序。
AST 的应用:
– 编译器:编译器使用 AST 进行语法分析、优化和代码生成。
– 静态代码分析:通过分析 AST,可以检查代码中的潜在问题或不符合编码规范的部分。
– 代码转换:通过修改 AST 可以实现代码重构、优化或跨平台代码生成。
2. AST语法树的生成
AST(抽象语法树)解析过程是将源代码转换为抽象语法树的过程,通常由词法分析、语法分析和语义分析三个主要步骤组成。每个步骤都对源代码进行处理,并逐步构建出程序的抽象语法树。

1. 词法分析(Lexical Analysis)
词法分析是将源代码的字符流转换成一系列的词法单元(Token),每个 Token 代表源代码中的一个基本成分,如关键字、标识符、运算符、常量、分隔符等。
步骤:
1. 输入源代码字符串。
2. 根据预定义的规则(如正则表达式),将代码切分成 Token。
3. 输出 Token 流(词法单元列表)。
2. 语法分析(Syntax Analysis)
语法分析将词法分析生成的 Token 序列根据语法规则(通常是上下文无关文法)构建出抽象语法树(AST)。语法分析器(Parser)会根据这些语法规则来判断代码是否符合程序语言的语法规范,并生成 AST。
步骤:
1. 输入:从词法分析器输出的 Token 流。
2. 使用文法规则(例如,BNF 或 EBNF)对 Token 流进行匹配。
3. 构建抽象语法树,其中每个节点表示一个语法元素,如操作符、操作数等。
3. 语义分析(Semantic Analysis)
语义分析用于检查程序的语义正确性,即确保程序的逻辑和类型符合语言的规则。例如,类型检查、变量作用域检查等。
步骤:
1. 输入:从语法分析得到的 AST。
2. 检查类型匹配、变量的定义和使用、操作符的适用性等。
3. 语义分析器对 AST 进行增强,确保程序的逻辑正确。
4. 生成 AST
最终的 AST 由语法分析器生成。在 AST 中,节点的结构与源代码的语法结构紧密相关。
解析过程总结:
1. 词法分析:将源代码拆分成一系列 Token。
2. 语法分析:根据语法规则将 Token 序列转换为抽象语法树(AST)。
3. 语义分析:检查语义错误,增强 AST。
3. Golang AST
了解了AST语法树的定义及生成过程,我们就需要深入Golang语言中了解Golang对于AST语法树的实现及定义。
1. Golang AST定义
Golang中AST相关的Package一共有四个,分别是:
"go/ast" // Package ast declares the types used to represent syntax trees for Go
"go/token" // Package token defines constants representing the lexical tokens of the Go
"go/parser" // Package parser implements a parser for Go source files.
"go/format" // Package format implements standard formatting of Go source.
“go/ast”包定义了AST相关的接口及结构体以及AST树遍历的相关方法
“go/token”包实现了AST语法树中token的常量定义记忆token相关方法
“go/parser”包实现了源码解析为AST语法树的解析器相关能力
“go/format”包主要用于实现对生成代码文本的格式化
既然AST是抽象语法树,那必然由一系列的树节点组成,在Golang语言中AST的节点分为三个组成部分:
ast.Expr – 代表表达式和类型的节点 (例如:字面量、变量、操作符等。)
ast.Stmt – 代表语句类型节点(例如:赋值、条件语句、循环语句等)
ast.Decl – 代表声明节点(例如:import,struct,interface,func)
这三个接口都继承了ast.Node接口,作为AST中所有结构的基类

ast.Stmt 所有子类
- 语法错误:BadStmt
- 声明语句:DeclStmt
- 空白语句:EmptyStmt
- 标签语句:LabeledStmt
- 表达式语句:ExprStmt
- chan发送语句:SendStmt
- 自增语句:IncDecStmt
- 赋值语句:AssignStmt
- 协程语句:GoStmt
- defer语句:DeferStmt
- 返回语句:ReturnStmt
- 分支语句:BranchStmt (break,continue,goto)
- 块语句:BlockStmt
- if语句:IfStmt (if else)
- case语句:CaseClause
- switch语句:SwitchStmt
- 类型转换语句:TypeSwitchStmt
- switch-case冒号:CommClause
- select语句:SelectStmt
- for循环语句:ForStmt
- range语句:RangeStmt
ast.Expr
- 语法错误:BadExpr
- 数组表声明:ArrayType
- 结构体声明:StructType
- 函数类型声明:FuncType
- 接口声明:InterfaceType
- map声明:MapType
- chan声明:ChanType(chan,<-)
- 标识名:Ident
- 结构表达式:Ellipsis
- 基本类型声明:BasicLit(token.INT, token.FLOAT, token.IMAG, token.CHAR, or token.STRING)
- 函数字面量(匿名):FuncLit
- 复合字面量:CompositeLit
- 括号表达式:ParenExpr
- 选择表达式:SelectorExpr
- 索引表达式:IndexExpr
- 多维索引表达式:IndexListExpr
- 切片表达式:SliceExpr
- 隐式类型转换表达式:TypeAssertExpr
- 函数调用表达式:CallExpr
- 指针表达式:StarExpr
- 一元表达式:UnaryExpr(a*=b a+=b ...)
- 二元表达式:BinaryExpr(c:=a+b)
- 键值表达式:KeyValueExpr
ast.Decl
- 错误声明:BadDecl
- 常用声明:GenDecl(token.IMPORT,token.CONST,token.TYPE,token.VAR)
- 函数声明:FuncDecl
Golang go文件解析后的生成的结构体定义
//ast.File
type File struct {
Doc *CommentGroup // associated documentation; or nil
Package token.Pos // position of "package" keyword
Name *Ident // package name
Decls []Decl // top-level declarations; or nil
FileStart, FileEnd token.Pos // start and end of entire file
Scope *Scope // package scope (this file only). Deprecated: see Object
Imports []*ImportSpec // imports in this file
Unresolved []*Ident // unresolved identifiers in this file. Deprecated: see Object
Comments []*CommentGroup // list of all comments in the source file
GoVersion string // minimum Go version required by //go:build or // +build directives
}
其中重要的部分为:Decls包含了所有变量、结构体及函数等内容的定义,Imports包含了所有引用的包,Comments包含了所有注释内容。要想通过构建AST实现生成Golang代码文件,就需要构建完整的ast.File结构体,至于怎么做还需要我们先了解如何使用golang上述三个包中的api去解析一个go文件看看具体构成。
2. Golang AST解析范例
首先上一段简单的golang代码
package hello
import "fmt"
func greet() {
fmt.Println("Hello World!")
}
通过如下代码可以对上述的代码进行解析
package main
import (
"go/ast"
"go/parser"
"go/token"
)
func main() {
src := `
package hello
import "fmt"
func greet() {
fmt.Println("Hello World!")
}
`
// Create the AST by parsing src.
fset := token.NewFileSet() // positions are relative to fset
f, err := parser.ParseFile(fset, "", src, 0)
if err != nil {
panic(err)
}
// Print the AST.
ast.Print(fset, f)
}
编译执行后输出如下内容:
0 *ast.File {
1 . Package: 2:1
2 . Name: *ast.Ident {
3 . . NamePos: 2:9
4 . . Name: "hello"
5 . }
6 . Decls: []ast.Decl (len = 2) {
7 . . 0: *ast.GenDecl {
8 . . . TokPos: 4:1
9 . . . Tok: import
10 . . . Lparen: -
11 . . . Specs: []ast.Spec (len = 1) {
12 . . . . 0: *ast.ImportSpec {
13 . . . . . Path: *ast.BasicLit {
14 . . . . . . ValuePos: 4:8
15 . . . . . . Kind: STRING
16 . . . . . . Value: "\"fmt\""
17 . . . . . }
18 . . . . . EndPos: -
19 . . . . }
20 . . . }
21 . . . Rparen: -
22 . . }
23 . . 1: *ast.FuncDecl {
24 . . . Name: *ast.Ident {
25 . . . . NamePos: 6:6
26 . . . . Name: "greet"
27 . . . . Obj: *ast.Object {
28 . . . . . Kind: func
29 . . . . . Name: "greet"
30 . . . . . Decl: *(obj @ 23)
31 . . . . }
32 . . . }
33 . . . Type: *ast.FuncType {
34 . . . . Func: 6:1
35 . . . . Params: *ast.FieldList {
36 . . . . . Opening: 6:11
37 . . . . . Closing: 6:12
38 . . . . }
39 . . . }
40 . . . Body: *ast.BlockStmt {
41 . . . . Lbrace: 6:14
42 . . . . List: []ast.Stmt (len = 1) {
43 . . . . . 0: *ast.ExprStmt {
44 . . . . . . X: *ast.CallExpr {
45 . . . . . . . Fun: *ast.SelectorExpr {
46 . . . . . . . . X: *ast.Ident {
47 . . . . . . . . . NamePos: 7:2
48 . . . . . . . . . Name: "fmt"
49 . . . . . . . . }
50 . . . . . . . . Sel: *ast.Ident {
51 . . . . . . . . . NamePos: 7:6
52 . . . . . . . . . Name: "Println"
53 . . . . . . . . }
54 . . . . . . . }
55 . . . . . . . Lparen: 7:13
56 . . . . . . . Args: []ast.Expr (len = 1) {
57 . . . . . . . . 0: *ast.BasicLit {
58 . . . . . . . . . ValuePos: 7:14
59 . . . . . . . . . Kind: STRING
60 . . . . . . . . . Value: "\"Hello World!\""
61 . . . . . . . . }
62 . . . . . . . }
63 . . . . . . . Ellipsis: -
64 . . . . . . . Rparen: 7:28
65 . . . . . . }
66 . . . . . }
67 . . . . }
68 . . . . Rbrace: 8:1
69 . . . }
70 . . }
71 . }
72 . Scope: *ast.Scope {
73 . . Objects: map[string]*ast.Object (len = 1) {
74 . . . "greet": *(obj @ 27)
75 . . }
76 . }
77 . Imports: []*ast.ImportSpec (len = 1) {
78 . . 0: *(obj @ 12)
79 . }
80 . Unresolved: []*ast.Ident (len = 1) {
81 . . 0: *(obj @ 46)
82 . }
83 }
通过上述的示例,我们对于AST的结构以及源码到AST的映射关系有了较为直观的认识,AST并不神秘,它是由一系列结构及关系来表达代码的一种方式,方便编译程序去理解源码的内容以及进行后续的编译操作将之生成为计算机可以识别的二进制编码。
上面的示例仅仅知识对AST进行了打印,想要精准的对AST语法树进行操作来达到修改代码的目的需要使用其他的API来进行。对于修改解析源码的应用场景,可以使用“go/ast”包下的Inspect方法对AST语法树的节点进行递归遍历从而达到读取所有节点进行自定义调整的目的。也可以使用定义结构体的方式从变量、表达式、方法逐级的构建出想要的源码并通过“go/format”包或“go/printer”包将AST转化为源码字符串。
至此我们基本上明确了AST的基本概念以及Golang中使用AST的基本操作及API,剩余的就是结合具体的应用场景来对API进行组合实现各种魔术操作,这就需要各位自己发挥创造力来探索了。这里也会提供一个利用AST实现Golang语言注解能力的应用场景,下面将会具体进行介绍。
二、基于AST实现Golang注解
1. 什么是注解
注解(Annotation)是一种用于提供元数据的机制,通常用于编程语言中。在程序源代码中,注解是对代码的附加信息,它本身并不会影响代码的执行结果,但可以用于工具或框架进行处理。注解通常用于描述代码的行为、属性或提供额外的配置和说明。最典型的采用注解进行编程的语言就是Java。例如常用的springboot框架内部集成了大量的注解用于实现各类框架功能,极大的简化了开发和重复性编程。
注解的实现原理依赖于编译时或运行时对注解的处理机制。具体的实现方式会依据编程语言的不同有所变化,下面以 Java 为例进行说明。
1. 注解的定义
注解是通过关键字 @interface 在 Java 中定义的。例如:
public @interface MyAnnotation {
String value() default "default value";
}
这定义了一个名为 MyAnnotation 的注解,它有一个名为 value 的成员,且具有默认值。
2. 注解的使用
注解可以应用到类、方法、字段、参数等地方。例如:
@MyAnnotation(value = "example")
public class MyClass {
@MyAnnotation
public void myMethod() {
// Method implementation
}
}
这里,MyAnnotation 被用来注解类 MyClass 和方法 myMethod。
3. 注解的处理
注解本身并不直接影响程序的执行,它需要通过某些机制进行处理。这些机制可以分为两种:
a. 编译时处理(Compile-Time Processing)
一些注解会在编译时通过工具(如 apt,Annotation Processing Tool)进行处理。例如,在编译时,编译器可以通过注解来生成代码、警告或者检查错误。
Java 提供了一个标准的注解处理工具 APT(Annotation Processing Tool),可以在编译时解析注解,执行特定的逻辑。
b. 运行时处理(Runtime Processing)
有些注解会在程序运行时通过反射机制进行处理。通过反射,程序可以访问类、方法、字段等的注解,从而执行相应的操作。
在 Java 中,可以通过 Class.getAnnotations()、Method.getAnnotations() 等方法获取类或方法上的注解。例如:
Method method = MyClass.class.getMethod("myMethod");
if (method.isAnnotationPresent(MyAnnotation.class)) {
MyAnnotation annotation = method.getAnnotation(MyAnnotation.class);
System.out.println(annotation.value());
}
4. 注解的生命周期
注解有不同的生命周期,它决定了注解何时有效。生命周期通常有以下几种:
– SOURCE:注解只存在于源代码中,在编译时被丢弃。
– CLASS:注解保留到字节码中,默认情况下,注解的保留策略是 CLASS,它可以被编译器处理,但不一定在运行时可用。
– RUNTIME:注解在运行时依然存在,可以通过反射获取到。
定义注解时,可以使用 @Retention 来指定注解的生命周期:
@Retention(RetentionPolicy.RUNTIME) // 使注解在运行时可用
public @interface MyAnnotation {}
5. 注解的元注解
元注解是用于定义其他注解的注解。例如:
@Retention:定义注解的生命周期。
@Target:定义注解的适用范围(类、方法、字段等)。
@Documented:指示注解应包含在 Java API 文档中。
@Inherited:表示子类可以继承父类的注解。
总结
注解的实现原理基于两个核心机制:
元数据存储:注解本身作为元数据被存储在源代码、字节码或运行时环境中。
注解处理:通过编译时工具或运行时反射对注解进行处理,达到特定功能(如代码生成、逻辑执行等)。
在实际应用中,注解常常结合反射、工具生成、框架设计等技术来增强代码的灵活性和可配置性。
2. 如何定义一个Golang注解
由于我们无法自定义golang的语法,所以想要实现注解最好的方式就是通过注释的方式,采用特殊的格式进行注释,并在注释中添加我们需要的元数据来实现类似注解的表达方式,并通过AST语法解析读取注释并根据我们定义的特殊格式来提取注解的信息以及被注解的文件、方法等内容从而实现注解想要表达的功能。
AST语法树解析出的注释都是纯文本的,因此我们需要根据自身需求来定义格式以便进行正则匹配以提取注释中的必要信息。
1.定义什么是注解
- 注解标记
根据上述的了解,我们知道Java中的注解由“@”来标记的,因此同样的,我们也需要定义一个标识该注释是注解的符号或特殊内容,这里我们采用golang中比较通用的格式 “go:”来标记 - 注解类型
注解类型也就是我们要定义注解是用来干什么的,或者说想要对注解标记的对象做出什么操作,例如Java中“@Override”注解标识对方法或字段等进行重写。这里我们在golang中选择构建一个范例:
定义 “go:controller” 用来标记go文件为一个web接口控制层,标识这个go文件中定义了各种的REST接口;
定义 “go:interface” 用来标记一个golang编写的RESTful接口
// go:controller
package server
import (
"any"
)
// go:interface
func (srv *Server) Hello(ctx server.Ctx) error {
// do something
}
- 注解参数
但一个注解可能并不能满足我们的开发需要,所以给注解添加参数支持就很有必要了,这里需要注意的是,由于注解的提取核心能力靠的是正则提取,因此,在进行参数命名时需要进行参数名的显示定义,以方便AST解析过程中能够争取的提取到需要的内容。
// go:controller(path="/hello",name="hello")
package server
import (
"any"
)
// go:interface(method="GET",path="/greet",auth="*",name="greet")
func (srv *Server) Hello(ctx server.Ctx) error {
// do something
}
上述例子中,我们对注解添加了参数定义,这样我们就得到了一个类似SpringBoot框架controller定义的go文件,从注解中,我们可以明确的知道这是一个path为/hello的接口层,并且定义了一个名为greet的GET接口,访问路径是“/hello/greet”,并且接口访问权限是 “*” 标识所有人都可以访问。
通过如上三步,我们定义了两个web接口的注解,下面就让我们来提取和实现注解的能力。