Go 抽象語法樹

jacobxy發表於2021-10-08

抽象語法樹 go/ast 庫使用

推薦背景

Go 語言在編譯過程中經過詞法分析和語法分析之後,就到了抽象語法樹的構建階段,經歷這一階段之後,語句就真正組織成了程式程式碼。利用抽象語法樹解析庫,我們可以完成程式碼的自動化分析和自動化生成,因此通常用於做一些自動化的工具,例如 wire。

使用案例

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 的第二引數為檔名,第三引數為字串,前者是待解析的檔案路徑,後者為待解析的字串。前者優先順序高於後者。第四引數為解析模式 (可以使用"|"來結合多種解析模式)

執行結果如下

 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 內容

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 成員表示的就是檔案中的頂級宣告。接下來我們主要是關注它的內容。

包宣告

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

對應檔案中的 "package main"語句,記錄了語句的位置以及包名 (main) 字串的位置資訊。

引入宣告

   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 的包名及位置資訊。

函式宣告

*ast.FuncDecl{
    Name:*ast.Index{...} 
    Type:*ast.FuncType{
        Params:*ast.FieldList{...}
        Results: *ast.FieldList{...}
    },
    Body:*ast.BlockStmt{...}
}
  • ast.FuncDecl 標明定義的是一個函式。
  • Name 記錄函式名
  • Type 是函式型別,其中 Params 表示引數資訊,這裡為空;Results 表示返回值資訊,這裡也為空。
  • Body 則為函式體資訊。

表示式

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 為處理節點的方法。

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 的特殊格式,來自定義框架程式碼的自動生成命令。

參考連線

https://www.cnblogs.com/double12gzh/p/13632267.html https://cloud.tencent.com/developer/section/1142075

實驗工具

https://astexplorer.net

https://greyireland.gitee.io/goast-viewer

更多原創文章乾貨分享,請關注公眾號
  • Go 抽象語法樹
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章