Skip to content

抽象语法树 go/ast 库使用 ​

推荐背景 ​

Go 语言在编译过程中经过词法分析和语法分析之后,就到了抽象语法树的构建阶段,经历这一阶段之后,语句就真正组织成了程序代码。利用抽象语法树解析库,我们可以完成代码的自动化分析和自动化生成,因此通常用于做一些自动化的工具,例如 wire。

这是默认主题内置的 <Badge /> 组件 演示

使用案例 ​

go
package main

import (
    "go/ast"
    "go/parser"
    "go/token"
)
var  src = `
	package main
	import "fmt"
	func main() {
    	fmt.Println("Hello, World!")
	}
`

func main() {
    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)
}
  • fset 是文件字符集用于定位 ast.Node 的文件位置
  • parser.ParserFile 的第二参数为文件名,第三参数为字符串,前者是待解析的文件路径,后者为待解析的字符串。前者优先级高于后者。第四参数为解析模式(可以使用”|“来结合多种解析模式)

运行结果如下

go
     0  *ast.File {
     1  .  Package: 2:1
     2  .  Name: *ast.Ident {
     3  .  .  NamePos: 2:9
     4  .  .  Name: "main"
     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: "main"
    27  .  .  .  .  Obj: *ast.Object {
    28  .  .  .  .  .  Kind: func
    29  .  .  .  .  .  Name: "main"
    30  .  .  .  .  .  Decl: *(obj @ 23)
    31  .  .  .  .  }
    32  .  .  .  }
    33  .  .  .  Type: *ast.FuncType {
    34  .  .  .  .  Func: 6:1
    35  .  .  .  .  Params: *ast.FieldList {
    36  .  .  .  .  .  Opening: 6:10
    37  .  .  .  .  .  Closing: 6:11
    38  .  .  .  .  }
    39  .  .  .  }
    40  .  .  .  Body: *ast.BlockStmt {
    41  .  .  .  .  Lbrace: 6:13
    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:5
    48  .  .  .  .  .  .  .  .  .  Name: "fmt"
    49  .  .  .  .  .  .  .  .  }
    50  .  .  .  .  .  .  .  .  Sel: *ast.Ident {
    51  .  .  .  .  .  .  .  .  .  NamePos: 7:9
    52  .  .  .  .  .  .  .  .  .  Name: "Println"
    53  .  .  .  .  .  .  .  .  }
    54  .  .  .  .  .  .  .  }
    55  .  .  .  .  .  .  .  Lparen: 7:16
    56  .  .  .  .  .  .  .  Args: []ast.Expr (len = 1) {
    57  .  .  .  .  .  .  .  .  0: *ast.BasicLit {
    58  .  .  .  .  .  .  .  .  .  ValuePos: 7:17
    59  .  .  .  .  .  .  .  .  .  Kind: STRING
    60  .  .  .  .  .  .  .  .  .  Value: "\"Hello World!\""
    61  .  .  .  .  .  .  .  .  }
    62  .  .  .  .  .  .  .  }
    63  .  .  .  .  .  .  .  Ellipsis: -
    64  .  .  .  .  .  .  .  Rparen: 7:31
    65  .  .  .  .  .  .  }
    66  .  .  .  .  .  }
    67  .  .  .  .  }
    68  .  .  .  .  Rbrace: 8:1
    69  .  .  .  }
    70  .  .  }
    71  .  }
    72  .  Scope: *ast.Scope {
    74  .  .  Objects: map[string]*ast.Object (len = 1) {
    74  .  .  .  "main": *(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.File 内容 ​

go
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
	Scope      *Scope          // package scope (this file only)
	Imports    []*ImportSpec   // imports in this file
	Unresolved []*Ident        // unresolved identifiers in this file
	Comments   []*CommentGroup // list of all comments in the source file
}

其中 Decls 成员表示的就是文件中的顶级声明。接下来我们主要是关注它的内容。

包声明 ​

go
  Package: 2:1
  Name: *ast.Ident {
  .  NamePos: 2:9
  .  Name: "main"
  }

对应文件中的 “package main”语句,记录了语句的位置以及包名(main)字符串的位置信息。

引入声明 ​

go
	0: *ast.GenDecl {
 .  TokPos: 4:1
 .  Tok: import
 .  Lparen: -
 .  Specs: []ast.Spec (len = 1) {
 .  .  0: *ast.ImportSpec {
 .  .  .  Path: *ast.BasicLit {
 .  .  .  .  ValuePos: 4:8
 .  .  .  .  Kind: STRING
 .  .  .  .  Value: "\"fmt\""
 .  .  .  }
 .  .  .  EndPos: -
 .  .  }
 .  }
 .  Rparen: -
 }

在 ast.GenDecl 中记录了 import 语句的位置信息。Specs 为一个 ast.Spec 的数组,记录了每一个 import 的包名及位置信息。

函数声明 ​

go
	*ast.FuncDecl{
		Name:*ast.Index{...}
		Type:*ast.FuncType{
			Params:*ast.FieldList{...}
			Results: *ast.FieldList{...}
		},
		Body:*ast.BlockStmt{...}
	}
  • ast.FuncDecl 标明定义的是一个函数。
  • Name 记录函数名
  • Type 是函数类型,其中 Params 表示参数信息,这里为空;Results 表示返回值信息,这里也为空。
  • Body 则为函数体信息。

表达式 ​

go
List: []ast.Stmt (len = 1) {
.  0: *ast.ExprStmt {
.  .  X: *ast.CallExpr {
.  .  .  Fun: *ast.SelectorExpr {
.  .  .  .  X: *ast.Ident {
.  .  .  .  .  NamePos: 7:5
.  .  .  .  .  Name: "fmt"
.  .  .  .  }
.  .  .  .  Sel: *ast.Ident {
.  .  .  .  .  NamePos: 7:9
.  .  .  .  .  Name: "Println"
.  .  .  .  }
.  .  .  }
.  .  .  Lparen: 7:16
.  .  .  Args: []ast.Expr (len = 1
.  .  .  .  0: *ast.BasicLit {
.  .  .  .  .  ValuePos: 7:17
.  .  .  .  .  Kind: STRING
.  .  .  .  .  Value: "\"Hello Wor
.  .  .  .  }
.  .  .  }
.  .  .  Ellipsis: -
.  .  .  Rparen: 7:31
.  .  }
.  }
}
Rbrace: 8:1

函数体中的表达式描述了函数的内容,例子中的 fmt.Println(“hello world”)。

ast.CallExpr 表示函数调用,其中 SelectorExpr 描述了调用函数的包名及函数名,Args 则描述了参数信息。

遍历 AST 树 ​

ast 库提供了可以深度优先遍历 AST 的方法:func Inspect(node Node, f func(Node) bool)。 其中 node 为根节点,f 为处理节点的方法。

go
ast.Inspect(f, func(n ast.Node) bool {
		var s string
		switch x := n.(type) {
		case *ast.BasicLit:
			s = x.Value
		case *ast.Ident:
			s = x.Name
		}
		if s != "" {
			fmt.Printf("%s:\t%s\n", fset.Position(n.Pos()), s)
		}
		return true
	})

此函数遍历 f(ast.File)节点打印所有的标识符和文字。

更多相关类型,可以通过命令

go doc ast |grep "^type .* struct"

查看。

进阶使用 ​

利用 AST 对 go 文件进行分析,我们可以实现代码的自动生成,其中包括以下几个常见使用领域:

  1. 代码注入: wire 使用 AST 实现构造函数代码生成。
  2. DeepCopy: 结合 AST 生成结构体的深拷贝函数代码
  3. 对象赋值: 在领域编程中,常常需要在不同的领域对象中进行数据转换,利用 AST 的解析结果,可以自动生成指定领域对象间的转换函数文件。

小结 ​

抽象语法树的生成属于程序编译流程中的一员,利用 AST 及其相关库提供到方法,我们可以很方便的解析一个 go 文件,把文件内容结构化,以便做进一步的分析和使用。AST 广泛应用于代码自动生成的功能中,例如

go generate

命令,wire 工具等等。其中不少企业也会在开源库中,使用 Comment 的特殊格式,来自定义框架代码的自动生成命令。