微信公眾號:[double12gzh]
關注容器技術、關注
Kubernetes。問題或建議,請公眾號留言。
寫在前面
當你對GoLang AST感興趣時,你會參考什么?文檔還是源代碼?
雖然閱讀文檔可以幫助你抽象地理解它,但你無法看到API之間的關系等等。
如果是閱讀整個源代碼,你會完全看懂,但你想看完整個代碼我覺得您應該會很累。
因此,本着高效學習的原則,我寫了此文,希望對您能有所幫助。
讓我們輕松一點,通過AST來了解我們平時寫的Go代碼在內部是如何表示的。
本文不深入探討如何解析源代碼,先從AST建立后的描述開始。
如果您對代碼如何轉換為AST很好奇,請瀏覽深入挖掘分析Go代碼。
讓我們開始吧!
接口(Interfaces)
首先,讓我簡單介紹一下代表AST每個節點的接口。
所有的AST節點都實現了ast.Node接口,它只是返回AST中的一個位置。
另外,還有3個主要接口實現了ast.Node。
- ast.Expr - 代表表達式和類型的節點
- ast.Stmt - 代表報表節點
- ast.Decl - 代表聲明節點

從定義中你可以看到,每個Node都滿足了ast.Node的接口。
// All node types implement the Node interface.
type Node interface {
Pos() token.Pos // position of first character belonging to the node
End() token.Pos // position of first character immediately after the node
}
// All expression nodes implement the Expr interface.
type Expr interface {
Node
exprNode()
}
// All statement nodes implement the Stmt interface.
type Stmt interface {
Node
stmtNode()
}
// All declaration nodes implement the Decl interface.
type Decl interface {
Node
declNode()
}
具體實踐
下面我們將使用到如下代碼:
package hello
import "fmt"
func greet() {
fmt.Println("Hello World!")
}
首先,我們嘗試生成上述這段簡單的代碼AST:
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)
}
執行命令:
F:\hello>go run main.go
上述命令的輸出ast.File內容如下:
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.Inspect()來逐一打印每個節點。
如果直接打印AST,那么我們通常會看到一些無法被人類閱讀的東西。
為了防止這種情況的發生,我們將使用ast.Print(一個強大的API)來實現對AST的人工讀取。
代碼如下:
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)
func main() {
fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "dummy.go", src, parser.ParseComments)
ast.Inspect(f, func(n ast.Node) bool {
// Called recursively.
ast.Print(fset, n)
return true
})
}
var src = `package hello
import "fmt"
func greet() {
fmt.Println("Hello, World")
}
`
ast.File
第一個要訪問的節點是*ast.File,它是所有AST節點的根。它只實現了ast.Node接口。

ast.File有引用包名、導入聲明和函數聲明作為子節點。
准確地說,它還有
Comments等,但為了簡單起見,我省略了它們。
讓我們從包名開始。
注意,帶nil值的字段會被省略。每個節點類型的完整字段列表請參見文檔。
包名
ast.Indent
*ast.Ident {
. NamePos: dummy.go:1:9
. Name: "hello"
}
一個包名可以用AST節點類型*ast.Ident來表示,它實現了ast.Expr接口。
所有的標識符都由這個結構來表示,它主要包含了它的名稱和在文件集中的源位置。
從上述所示的代碼中,我們可以看到包名是hello,並且是在dummy.go的第一行聲明的。
對於這個節點我們不會再深入研究了,讓我們再回到
*ast.File.Go中。
導入聲明
ast.GenDecl
*ast.GenDecl {
. TokPos: dummy.go:3:1
. Tok: import
. Lparen: -
. Specs: []ast.Spec (len = 1) {
. . 0: *ast.ImportSpec {/* Omission */}
. }
. Rparen: -
}
ast.GenDecl代表除函數以外的所有聲明,即import、const、var和type。
Tok代表一個詞性標記--它指定了聲明的內容(import或const或type或var)。
這個AST節點告訴我們,import聲明在dummy.go的第3行。
讓我們從上到下深入地看一下ast.GenDecl的下一個節點*ast.ImportSpec。
ast.ImportSpec
*ast.ImportSpec {
. Path: *ast.BasicLit {/* Omission */}
. EndPos: -
}
一個ast.ImportSpec節點對應一個導入聲明。它實現了ast.Spec接口,訪問路徑可以讓導入路徑更有意義。
ast.BasicLit
*ast.BasicLit {
. ValuePos: dummy.go:3:8
. Kind: STRING
. Value: "\"fmt\""
}
一個ast.BasicLit節點表示一個基本類型的文字,它實現了ast.Expr接口。
它包含一個token類型,可以使用token.INT、token.FLOAT、token.IMAG、token.CHAR或token.STRING。
從ast.ImportSpec和ast.BasicLit中,我們可以看到它導入了名為"fmt "的包。
我們不再深究了,讓我們再回到頂層。
函數聲明
ast.FuncDecl
*ast.FuncDecl {
. Name: *ast.Ident {/* Omission */}
. Type: *ast.FuncType {/* Omission */}
. Body: *ast.BlockStmt {/* Omission */}
}
一個ast.FuncDecl節點代表一個函數聲明,但它只實現了ast.Node接口。我們從代表函數名的Name開始,依次看一下。
ast.Ident
*ast.Ident {
. NamePos: dummy.go:5:6
. Name: "greet"
. Obj: *ast.Object {
. . Kind: func
. . Name: "greet"
. . Decl: *(obj @ 0)
. }
}
第二次出現這種情況,我就不做基本解釋了。
值得注意的是*ast.Object,它代表了標識符所指的對象,但為什么需要這個呢?
大家知道,GoLang有一個scope的概念,就是源文本的scope,其中標識符表示指定的常量、類型、變量、函數、標簽或包。
Decl字段表示標識符被聲明的位置,這樣就確定了標識符的scope。指向相同對象的標識符共享相同的*ast.Object.Label。
ast.FuncType
*ast.FuncType {
. Func: dummy.go:5:1
. Params: *ast.FieldList {/* Omission */}
}
一個 ast.FuncType 包含一個函數簽名,包括參數、結果和 "func "關鍵字的位置。
ast.FieldList
*ast.FieldList {
. Opening: dummy.go:5:11
. List: nil
. Closing: dummy.go:5:12
}
ast.FieldList節點表示一個Field的列表,用括號或大括號括起來。如果定義了函數參數,這里會顯示,但這次沒有,所以沒有信息。
列表字段是*ast.Field的一個切片,包含一對標識符和類型。它的用途很廣,用於各種Nodes,包括*ast.StructType、*ast.InterfaceType和本文中使用示例。
也就是說,當把一個類型映射到一個標識符時,需要用到它(如以下的代碼):
foot int
bar string
讓我們再次回到*ast.FuncDecl,再深入了解一下最后一個字段Body。
ast.BlockStmt
*ast.BlockStmt {
. Lbrace: dummy.go:5:14
. List: []ast.Stmt (len = 1) {
. . 0: *ast.ExprStmt {/* Omission */}
. }
. Rbrace: dummy.go:7:1
}
一個ast.BlockStmt節點表示一個括號內的語句列表,它實現了ast.Stmt接口。
ast.ExprStmt
*ast.ExprStmt {
. X: *ast.CallExpr {/* Omission */}
}
ast.ExprStmt在語句列表中表示一個表達式,它實現了ast.Stmt接口,並包含一個ast.Expr。
ast.CallExpr
*ast.CallExpr {
. Fun: *ast.SelectorExpr {/* Omission */}
. Lparen: dummy.go:6:13
. Args: []ast.Expr (len = 1) {
. . 0: *ast.BasicLit {/* Omission */}
. }
. Ellipsis: -
. Rparen: dummy.go:6:28
}
ast.CallExpr表示一個調用函數的表達式,要查看的字段是:
- Fun
- 要調用的函數和Args
- 要傳遞給它的參數列表
ast.SelectorExpr
*ast.SelectorExpr {
. X: *ast.Ident {
. . NamePos: dummy.go:6:2
. . Name: "fmt"
. }
. Sel: *ast.Ident {
. . NamePos: dummy.go:6:6
. . Name: "Println"
. }
}
ast.SelectorExpr表示一個帶有選擇器的表達式。簡單地說,它的意思是fmt.Println。
ast.BasicLit
*ast.BasicLit {
. ValuePos: dummy.go:6:14
. Kind: STRING
. Value: "\"Hello, World\""
}
這個就不需要多解釋了,就是簡單的"Hello, World。
小結
需要注意的是,在介紹的節點類型時,節點類型中的一些字段及很多其它的節點類型都被我省略了。
盡管如此,我還是想說,即使有點粗糙,但實際操作一下還是很有意義的,而且最重要的是,它是相當有趣的。
復制並粘貼本文第一節中所示的代碼,在你的電腦上試着實操一下吧。
