對 Go 代碼進行分析,常用這個技術:AST
寫在前面
當你對 Golang AST 感興趣時,你會參考什麼?文檔還是源代碼?
雖然閱讀文檔可以幫助你抽象地理解它,但你無法看到 API 之間的關係等等。
如果是閱讀整個源代碼,你會完全看懂,但你想看完整個代碼我覺得您應該會很累。
因此,本着高效學習的原則,我寫了此文,希望對您能有所幫助。
讓我們輕鬆一點,通過 AST 來了解我們平時寫的 Go 代碼在內部是如何表示的。
本文不深入探討如何解析源代碼,先從 AST 建立後的描述開始。
如果您對代碼如何轉換爲 AST 很好奇,請瀏覽深入挖掘分析 Go 代碼 [1]。
讓我們開始吧!
接口 (Interfaces)
首先,讓我簡單介紹一下代表 AST 每個節點的接口。
所有的 AST 節點都實現了 ast.Node
接口,它只是返回 AST 中的一個位置。
另外,還有 3 個主要接口實現了 ast.Node
。
-
ast.Expr - 代表表達式和類型的節點
-
ast.Stmt - 代表報表節點
-
ast.Decl - 代表聲明節點
從定義中你可以看到,每個 Node 都滿足了 ast.Node
的接口。
ast/ast.go[2]
1// All node types implement the Node interface.
2type 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
7// All expression nodes implement the Expr interface.
8type Expr interface {
9 Node
10 exprNode()
11}
12
13// All statement nodes implement the Stmt interface.
14type Stmt interface {
15 Node
16 stmtNode()
17}
18
19// All declaration nodes implement the Decl interface.
20type Decl interface {
21 Node
22 declNode()
23}
24
25
具體實踐
下面我們將使用到如下代碼:
1package hello
2
3import "fmt"
4
5func greet() {
6 fmt.Println("Hello World!")
7}
8
9
首先,我們嘗試生成上述這段簡單的代碼 AST[3]:
1package main
2
3import (
4 "go/ast"
5 "go/parser"
6 "go/token"
7)
8
9func main() {
10 src := `
11package hello
12
13import "fmt"
14
15func greet() {
16 fmt.Println("Hello World!")
17}
18`
19 // Create the AST by parsing src.
20 fset := token.NewFileSet() // positions are relative to fset
21 f, err := parser.ParseFile(fset, "", src, 0)
22 if err != nil {
23 panic(err)
24 }
25
26 // Print the AST.
27 ast.Print(fset, f)
28}
29
30
執行命令:
1F:\hello>go run main.go
2
3
上述命令的輸出 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 }
85
86
如何分析
我們要做的就是按照深度優先的順序遍歷這個 AST 節點,通過遞歸調用 ast.Inspect()
來逐一打印每個節點。
如果直接打印 AST,那麼我們通常會看到一些無法被人類閱讀的東西。
爲了防止這種情況的發生,我們將使用 ast.Print
(一個強大的 API) 來實現對 AST 的人工讀取。
代碼如下:
1package main
2
3import (
4 "fmt"
5 "go/ast"
6 "go/parser"
7 "go/token"
8)
9
10func main() {
11 fset := token.NewFileSet()
12 f, _ := parser.ParseFile(fset, "dummy.go", src, parser.ParseComments)
13
14 ast.Inspect(f, func(n ast.Node) bool {
15 // Called recursively.
16 ast.Print(fset, n)
17 return true
18 })
19}
20
21var src = `package hello
22
23import "fmt"
24
25func greet() {
26 fmt.Println("Hello, World")
27}
28`
29
30
ast.File
第一個要訪問的節點是 *ast.File
,它是所有 AST 節點的根。它只實現了 ast.Node
接口。
ast.File
有引用 包名
、導入聲明
和 函數聲明
作爲子節點。
準確地說,它還有
Comments
等,但爲了簡單起見,我省略了它們。
讓我們從包名開始。
注意,帶 nil 值的字段會被省略。每個節點類型的完整字段列表請參見文檔。
包名
ast.Indent
1*ast.Ident {
2. NamePos: dummy.go:1:9
3. Name: "hello"
4}
5
6
一個包名可以用 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}
10
11
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}
5
6
一個 ast.ImportSpec
節點對應一個導入聲明。它實現了 ast.Spec
接口,訪問路徑可以讓導入路徑更有意義。
ast.BasicLit
1*ast.BasicLit {
2. ValuePos: dummy.go:3:8
3. Kind: STRING
4. Value: "\"fmt\""
5}
6
7
一個 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}
6
7
一個 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}
10
11
第二次出現這種情況,我就不做基本解釋了。
值得注意的是 *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}
5
6
一個 ast.FuncType
包含一個函數簽名,包括參數、結果和 "func" 關鍵字的位置。
ast.FieldList
1*ast.FieldList {
2. Opening: dummy.go:5:11
3. List: nil
4. Closing: dummy.go:5:12
5}
6
7
ast.FieldList
節點表示一個 Field 的列表,用括號或大括號括起來。如果定義了函數參數,這裏會顯示,但這次沒有,所以沒有信息。
列表字段是 *ast.Field
的一個切片,包含一對標識符和類型。它的用途很廣,用於各種 Nodes,包括 *ast.StructType
、*ast.InterfaceType
和本文中使用示例。
也就是說,當把一個類型映射到一個標識符時,需要用到它(如以下的代碼):
1foot int
2bar string
3
4
讓我們再次回到 *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}
8
9
一個 ast.BlockStmt
節點表示一個括號內的語句列表,它實現了 ast.Stmt
接口。
ast.ExprStmt
1*ast.ExprStmt {
2. X: *ast.CallExpr {/* Omission */}
3}
4
5
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}
10
11
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}
11
12
ast.SelectorExpr
表示一個帶有選擇器的表達式。簡單地說,它的意思是 fmt.Println
。
ast.BasicLit
1*ast.BasicLit {
2. ValuePos: dummy.go:6:14
3. Kind: STRING
4. Value: "\"Hello, World\""
5}
6
7
這個就不需要多解釋了,就是簡單的 "Hello, World。
小結
需要注意的是,在介紹的節點類型時,節點類型中的一些字段及很多其它的節點類型都被我省略了。
儘管如此,我還是想說,即使有點粗糙,但實際操作一下還是很有意義的,而且最重要的是,它是相當有趣的。
複製並粘貼本文第一節中所示的代碼,在你的電腦上試着實操一下吧。
via: https://nakabonne.dev/posts/take-a-walk-the-go-ast/
作者:nakabonne[4] 譯者:double12gzh[5] 校對:polaris1119[6]
本文由 GCTT[7] 原創編譯,Go 中文網 [8] 榮譽推出
參考資料
[1]
深入挖掘分析 Go 代碼: https://nakabonne.dev/posts/digging-deeper-into-the-analysis-of-go-code/
[2]
ast/ast.go: https://github.com/golang/go/blob/0b7c202e98949b530f7f4011efd454164356ba69/src/go/ast/ast.go#L32-L54
[3]
生成上述這段簡單的代碼 AST: https://golang.org/src/go/ast/example_test.go
[4]
nakabonne: https://github.com/nakabonne
[5]
double12gzh: https://github.com/double12gzh
[6]
polaris1119: https://github.com/polaris1119
[7]
GCTT: https://github.com/studygolang/GCTT
[8]
Go 中文網: https://studygolang.com/
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/gKnaHxoiBZrBMsXKsz2kvw