GoLang AST簡介


微信公眾號:[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的接口。

ast/ast.go

// 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代表除函數以外的所有聲明,即importconstvartype

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.ImportSpecast.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。

小結

需要注意的是,在介紹的節點類型時,節點類型中的一些字段及很多其它的節點類型都被我省略了。

盡管如此,我還是想說,即使有點粗糙,但實際操作一下還是很有意義的,而且最重要的是,它是相當有趣的。

復制並粘貼本文第一節中所示的代碼,在你的電腦上試着實操一下吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM