對 Go 代碼進行分析,常用這個技術:AST

寫在前面

當你對 Golang AST 感興趣時,你會參考什麼?文檔還是源代碼?

雖然閱讀文檔可以幫助你抽象地理解它,但你無法看到 API 之間的關係等等。

如果是閱讀整個源代碼,你會完全看懂,但你想看完整個代碼我覺得您應該會很累。

因此,本着高效學習的原則,我寫了此文,希望對您能有所幫助。

讓我們輕鬆一點,通過 AST 來了解我們平時寫的 Go 代碼在內部是如何表示的。

本文不深入探討如何解析源代碼,先從 AST 建立後的描述開始。

如果您對代碼如何轉換爲 AST 很好奇,請瀏覽深入挖掘分析 Go 代碼 [1]。

讓我們開始吧!

接口 (Interfaces)

首先,讓我簡單介紹一下代表 AST 每個節點的接口。

所有的 AST 節點都實現了 ast.Node 接口,它只是返回 AST 中的一個位置。

另外,還有 3 個主要接口實現了 ast.Node

從定義中你可以看到,每個 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 代表除函數以外的所有聲明,即 importconstvartype

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.ImportSpecast.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 表示一個調用函數的表達式,要查看的字段是:

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