写在前面

当你对GoLang AST感兴趣时,你会参考什么?文档还是源代码?
虽然阅读文档可以帮助你抽象地理解它,但你无法看到API之间的关系等等。
如果是阅读整个源代码,你会完全看懂,但你想看完整个代码我觉得您应该会很累。
因此,本着高效学习的原则,我写了此文,希望对您能有所帮助。
让我们轻松一点,通过AST来了解我们平时写的Go代码在内部是如何表示的。
本文不深入探讨如何解析源代码,先从AST建立后的描述开始。
如果您对代码如何转换为AST很好奇,请浏览深入挖掘分析Go代码
让我们开始吧!

接口(Interfaces)

首先,让我简单介绍一下代表AST每个节点的接口。
所有的AST节点都实现了ast.Node接口,它只是返回AST中的一个位置。
另外,还有3个主要接口实现了ast.Node。

  • ast.Expr - 代表表达式和类型的节点
  • ast.Stmt - 代表报表节点
  • ast.Decl - 代表声明节点

每日一库之125:ast(抽象语法树) - 图1
从定义中你可以看到,每个Node都满足了ast.Node的接口。
ast/ast.go

  1. // All node types implement the Node interface.
  2. type Node interface {
  3. Pos() token.Pos // position of first character belonging to the node
  4. End() token.Pos // position of first character immediately after the node
  5. }
  6. // All expression nodes implement the Expr interface.
  7. type Expr interface {
  8. Node
  9. exprNode()
  10. }
  11. // All statement nodes implement the Stmt interface.
  12. type Stmt interface {
  13. Node
  14. stmtNode()
  15. }
  16. // All declaration nodes implement the Decl interface.
  17. type Decl interface {
  18. Node
  19. declNode()
  20. }

具体实践

下面我们将使用到如下代码:

  1. package hello
  2. import "fmt"
  3. func greet() {
  4. fmt.Println("Hello World!")
  5. }

首先,我们尝试生成上述这段简单的代码AST

  1. package main
  2. import (
  3. "go/ast"
  4. "go/parser"
  5. "go/token"
  6. )
  7. func main() {
  8. src := `
  9. package hello
  10. import "fmt"
  11. func greet() {
  12. fmt.Println("Hello World!")
  13. }
  14. `
  15. // Create the AST by parsing src.
  16. fset := token.NewFileSet() // positions are relative to fset
  17. f, err := parser.ParseFile(fset, "", src, 0)
  18. if err != nil {
  19. panic(err)
  20. }
  21. // Print the AST.
  22. ast.Print(fset, f)
  23. }

执行命令:

  1. go run main.go

上述命令的输出ast.File内容如下:

  1. 0 *ast.File {
  2. 1 . Package: 2:1
  3. 2 . Name: *ast.Ident {
  4. 3 . . NamePos: 2:9
  5. 4 . . Name: "hello"
  6. 5 . }
  7. 6 . Decls: []ast.Decl (len = 2) {
  8. 7 . . 0: *ast.GenDecl {
  9. 8 . . . TokPos: 4:1
  10. 9 . . . Tok: import
  11. 10 . . . Lparen: -
  12. 11 . . . Specs: []ast.Spec (len = 1) {
  13. 12 . . . . 0: *ast.ImportSpec {
  14. 13 . . . . . Path: *ast.BasicLit {
  15. 14 . . . . . . ValuePos: 4:8
  16. 15 . . . . . . Kind: STRING
  17. 16 . . . . . . Value: "\"fmt\""
  18. 17 . . . . . }
  19. 18 . . . . . EndPos: -
  20. 19 . . . . }
  21. 20 . . . }
  22. 21 . . . Rparen: -
  23. 22 . . }
  24. 23 . . 1: *ast.FuncDecl {
  25. 24 . . . Name: *ast.Ident {
  26. 25 . . . . NamePos: 6:6
  27. 26 . . . . Name: "greet"
  28. 27 . . . . Obj: *ast.Object {
  29. 28 . . . . . Kind: func
  30. 29 . . . . . Name: "greet"
  31. 30 . . . . . Decl: *(obj @ 23)
  32. 31 . . . . }
  33. 32 . . . }
  34. 33 . . . Type: *ast.FuncType {
  35. 34 . . . . Func: 6:1
  36. 35 . . . . Params: *ast.FieldList {
  37. 36 . . . . . Opening: 6:11
  38. 37 . . . . . Closing: 6:12
  39. 38 . . . . }
  40. 39 . . . }
  41. 40 . . . Body: *ast.BlockStmt {
  42. 41 . . . . Lbrace: 6:14
  43. 42 . . . . List: []ast.Stmt (len = 1) {
  44. 43 . . . . . 0: *ast.ExprStmt {
  45. 44 . . . . . . X: *ast.CallExpr {
  46. 45 . . . . . . . Fun: *ast.SelectorExpr {
  47. 46 . . . . . . . . X: *ast.Ident {
  48. 47 . . . . . . . . . NamePos: 7:2
  49. 48 . . . . . . . . . Name: "fmt"
  50. 49 . . . . . . . . }
  51. 50 . . . . . . . . Sel: *ast.Ident {
  52. 51 . . . . . . . . . NamePos: 7:6
  53. 52 . . . . . . . . . Name: "Println"
  54. 53 . . . . . . . . }
  55. 54 . . . . . . . }
  56. 55 . . . . . . . Lparen: 7:13
  57. 56 . . . . . . . Args: []ast.Expr (len = 1) {
  58. 57 . . . . . . . . 0: *ast.BasicLit {
  59. 58 . . . . . . . . . ValuePos: 7:14
  60. 59 . . . . . . . . . Kind: STRING
  61. 60 . . . . . . . . . Value: "\"Hello World!\""
  62. 61 . . . . . . . . }
  63. 62 . . . . . . . }
  64. 63 . . . . . . . Ellipsis: -
  65. 64 . . . . . . . Rparen: 7:28
  66. 65 . . . . . . }
  67. 66 . . . . . }
  68. 67 . . . . }
  69. 68 . . . . Rbrace: 8:1
  70. 69 . . . }
  71. 70 . . }
  72. 71 . }
  73. 72 . Scope: *ast.Scope {
  74. 73 . . Objects: map[string]*ast.Object (len = 1) {
  75. 74 . . . "greet": *(obj @ 27)
  76. 75 . . }
  77. 76 . }
  78. 77 . Imports: []*ast.ImportSpec (len = 1) {
  79. 78 . . 0: *(obj @ 12)
  80. 79 . }
  81. 80 . Unresolved: []*ast.Ident (len = 1) {
  82. 81 . . 0: *(obj @ 46)
  83. 82 . }
  84. 83 }

如何分析

我们要做的就是按照深度优先的顺序遍历这个AST节点,通过递归调用ast.Inspect()来逐一打印每个节点。
如果直接打印AST,那么我们通常会看到一些无法被人类阅读的东西。
为了防止这种情况的发生,我们将使用ast.Print(一个强大的API)来实现对AST的人工读取。
代码如下:

  1. package main
  2. import (
  3. "fmt"
  4. "go/ast"
  5. "go/parser"
  6. "go/token"
  7. )
  8. func main() {
  9. fset := token.NewFileSet()
  10. f, _ := parser.ParseFile(fset, "dummy.go", src, parser.ParseComments)
  11. ast.Inspect(f, func(n ast.Node) bool {
  12. // Called recursively.
  13. ast.Print(fset, n)
  14. return true
  15. })
  16. }
  17. var src = `package hello
  18. import "fmt"
  19. func greet() {
  20. fmt.Println("Hello, World")
  21. }
  22. `

ast.File
第一个要访问的节点是*ast.File,它是所有AST节点的根。它只实现了ast.Node接口。
每日一库之125:ast(抽象语法树) - 图2
ast.File有引用包名、导入声明和函数声明作为子节点。

准确地说,它还有Comments等,但为了简单起见,我省略了它们。

让我们从包名开始。

注意,带nil值的字段会被省略。每个节点类型的完整字段列表请参见文档。

包名

ast.Indent

  1. *ast.Ident {
  2. . NamePos: dummy.go:1:9
  3. . Name: "hello"
  4. }

一个包名可以用AST节点类型*ast.Ident来表示,它实现了ast.Expr接口。
所有的标识符都由这个结构来表示,它主要包含了它的名称和在文件集中的源位置。
从上述所示的代码中,我们可以看到包名是hello,并且是在dummy.go的第一行声明的。

对于这个节点我们不会再深入研究了,让我们再回到*ast.File.Go中。

导入声明

ast.GenDecl

  1. *ast.GenDecl {
  2. . TokPos: dummy.go:3:1
  3. . Tok: import
  4. . Lparen: -
  5. . Specs: []ast.Spec (len = 1) {
  6. . . 0: *ast.ImportSpec {/* Omission */}
  7. . }
  8. . Rparen: -
  9. }

ast.GenDecl代表除函数以外的所有声明,即import、const、var和type。
Tok代表一个词性标记—它指定了声明的内容(import或const或type或var)。
这个AST节点告诉我们,import声明在dummy.go的第3行。
让我们从上到下深入地看一下ast.GenDecl的下一个节点ast.ImportSpec。
*ast.ImportSpec

  1. *ast.ImportSpec {
  2. . Path: *ast.BasicLit {/* Omission */}
  3. . EndPos: -
  4. }

一个ast.ImportSpec节点对应一个导入声明。它实现了ast.Spec接口,访问路径可以让导入路径更有意义。
ast.BasicLit

  1. *ast.BasicLit {
  2. . ValuePos: dummy.go:3:8
  3. . Kind: STRING
  4. . Value: "\"fmt\""
  5. }

一个ast.BasicLit节点表示一个基本类型的文字,它实现了ast.Expr接口。
它包含一个token类型,可以使用token.INT、token.FLOAT、token.IMAG、token.CHAR或token.STRING。
从ast.ImportSpec和ast.BasicLit中,我们可以看到它导入了名为”fmt “的包。
我们不再深究了,让我们再回到顶层。

函数声明

ast.FuncDecl

  1. *ast.FuncDecl {
  2. . Name: *ast.Ident {/* Omission */}
  3. . Type: *ast.FuncType {/* Omission */}
  4. . Body: *ast.BlockStmt {/* Omission */}
  5. }

一个ast.FuncDecl节点代表一个函数声明,但它只实现了ast.Node接口。我们从代表函数名的Name开始,依次看一下。
ast.Ident

  1. *ast.Ident {
  2. . NamePos: dummy.go:5:6
  3. . Name: "greet"
  4. . Obj: *ast.Object {
  5. . . Kind: func
  6. . . Name: "greet"
  7. . . Decl: *(obj @ 0)
  8. . }
  9. }

第二次出现这种情况,我就不做基本解释了。
值得注意的是ast.Object,它代表了标识符所指的对象,但为什么需要这个呢?
大家知道,GoLang有一个scope的概念,就是源文本的scope,其中标识符表示指定的常量、类型、变量、函数、标签或包。
Decl字段表示标识符被声明的位置,这样就确定了标识符的scope。指向相同对象的标识符共享相同的
ast.Object.Label。
ast.FuncType

  1. *ast.FuncType {
  2. . Func: dummy.go:5:1
  3. . Params: *ast.FieldList {/* Omission */}
  4. }

一个 ast.FuncType 包含一个函数签名,包括参数、结果和 “func “关键字的位置。
ast.FieldList

  1. *ast.FieldList {
  2. . Opening: dummy.go:5:11
  3. . List: nil
  4. . Closing: dummy.go:5:12
  5. }

ast.FieldList节点表示一个Field的列表,用括号或大括号括起来。如果定义了函数参数,这里会显示,但这次没有,所以没有信息。
列表字段是ast.Field的一个切片,包含一对标识符和类型。它的用途很广,用于各种Nodes,包括ast.StructType、*ast.InterfaceType和本文中使用示例。
也就是说,当把一个类型映射到一个标识符时,需要用到它(如以下的代码):

  1. foot int
  2. bar string

让我们再次回到ast.FuncDecl,再深入了解一下最后一个字段Body。
*ast.BlockStmt

  1. *ast.BlockStmt {
  2. . Lbrace: dummy.go:5:14
  3. . List: []ast.Stmt (len = 1) {
  4. . . 0: *ast.ExprStmt {/* Omission */}
  5. . }
  6. . Rbrace: dummy.go:7:1
  7. }

一个ast.BlockStmt节点表示一个括号内的语句列表,它实现了ast.Stmt接口。
ast.ExprStmt

  1. *ast.ExprStmt {
  2. . X: *ast.CallExpr {/* Omission */}
  3. }

ast.ExprStmt在语句列表中表示一个表达式,它实现了ast.Stmt接口,并包含一个ast.Expr。
ast.CallExpr

  1. *ast.CallExpr {
  2. . Fun: *ast.SelectorExpr {/* Omission */}
  3. . Lparen: dummy.go:6:13
  4. . Args: []ast.Expr (len = 1) {
  5. . . 0: *ast.BasicLit {/* Omission */}
  6. . }
  7. . Ellipsis: -
  8. . Rparen: dummy.go:6:28
  9. }

ast.CallExpr表示一个调用函数的表达式,要查看的字段是:

  • Fun
  • 要调用的函数和Args
  • 要传递给它的参数列表

ast.SelectorExpr

  1. *ast.SelectorExpr {
  2. . X: *ast.Ident {
  3. . . NamePos: dummy.go:6:2
  4. . . Name: "fmt"
  5. . }
  6. . Sel: *ast.Ident {
  7. . . NamePos: dummy.go:6:6
  8. . . Name: "Println"
  9. . }
  10. }

ast.SelectorExpr表示一个带有选择器的表达式。简单地说,它的意思是fmt.Println。
ast.BasicLit

  1. *ast.BasicLit {
  2. . ValuePos: dummy.go:6:14
  3. . Kind: STRING
  4. . Value: "\"Hello, World\""
  5. }

这个就不需要多解释了,就是简单的”Hello, World。

小结

需要注意的是,在介绍的节点类型时,节点类型中的一些字段及很多其它的节点类型都被我省略了。
尽管如此,我还是想说,即使有点粗糙,但实际操作一下还是很有意义的,而且最重要的是,它是相当有趣的。
复制并粘贴本文第一节中所示的代码,在你的电脑上试着实操一下吧。