這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
本文需要你有寫Golang代碼經驗,閱讀大概需要20分鐘。
最近一直在研究Go的依賴注入(dependencyinjection),方便日後寫比較容易測試的代碼(以便偷懶)。目前學到ast解析代碼,現拿出來跟大家分享一下:)
Tokenizer 和 Lexical anaylizer
如果你知道tokenizer和lexical anaylizer是什麼的話,請跳到下一章,不熟的話請看下面這個最簡單的go代碼
package mainfunc main() { println("Hello, World!")}
這段go代碼做了什嗎?很簡單吧,package是main,定義了個main函數,main函數裡調用了println函數,參數是”Hello,World!“。好,你是知道了,可當你運行gorun時,go怎麼知道的?go先要把你的代碼打散成自己可以理解的構成部分(token),這一過程就叫tokenize。例如,第一行就被拆成了package和main。這個階段,go就像小嬰兒只會理解我、要、吃飯等詞,但串不成合適句子。因為”吃飯我要”是講不通的,所以把詞按一定的文法串起來的過程就是lexicalanaylize或者parse,簡單吧!和人腦不同的是,被程式理解的代碼,通常會以abstract syntaxtree(AST)的形式儲存起來,方便進行校正和尋找。
Go的AST
那我們來看看go的ast庫對代碼的理解程度是不是小嬰兒吧(可啟動並執行原始碼在此),其實就是token+parse剛才我們看到的上一章代碼,並且按AST的方式列印出來,結果在這裡
package mainimport ( "go/ast" "go/parser" "go/token")func main() { // 這就是上一章的代碼. src := `package mainfunc main() { 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)}
為了不嚇到你,我先只列印前6行:
0 *ast.File { 1 . Package: 2:1 2 . Name: *ast.Ident { 3 . . NamePos: 2:9 4 . . Name: "main" 5 . } // 省略之後的50+行
可見,go解析出了package這個關鍵詞在文本的第二行的第一個(2:1)。”main”也解析出來了,在第二行的第9個字元,但是go的解析器還給它安了一個叫法:ast.Ident,標示符 或者大家常說的ID,如所示:
Ident +------------+ | Package +-----+ | v v package main
接下來我們看看那個main函數被整成了什麼樣。
6 . Decls: []ast.Decl (len = 1) { 7 . . 0: *ast.FuncDecl { 8 . . . Name: *ast.Ident { 9 . . . . NamePos: 3:6 10 . . . . Name: "main" 11 . . . . Obj: *ast.Object { 12 . . . . . Kind: func 13 . . . . . Name: "main" 14 . . . . . Decl: *(obj @ 7)
此處func main被解析成ast.FuncDecl(functiondeclaration),而函數的參數(Params)和函數體(Body)自然也在這個FuncDecl中。Params對應的是*ast.FieldList,顧名思義就是項列表;而由大括弧”{}”組成的函數體對應的是ast.BlockStmt(blockstatement)。如果不清楚,可以參考下面的圖:
FuncDecl.Params +----------+ | FuncDecl.Name +--------+ | v v +----------------------> func main() { | +->FuncDecl ++ FuncDecl.Body +-+ println("Hello, World!") | +-> +----------------------> }
而對於main函數的函數體中,我們可以看到調用了println函數,在ast中對應的是ExprStmt(ExpressStatement),調用函數的運算式對應的是CallExpr(CallExpression),調用的參數自然不能錯過,因為參數只有字串,所以go把它歸為ast.BasicLis (a literal of basictype)。如所示:
+-----+ ExprStmt +---------------+| || CallExpr BasicLit || + + || v v |+---> println("Hello, World!")<--+
還有什嗎?
50 . Scope: *ast.Scope { 51 . . Objects: map[string]*ast.Object (len = 1) { 52 . . . "main": *(obj @ 11) 53 . . } 54 . } 55 . Unresolved: []*ast.Ident (len = 1) { 56 . . 0: *(obj @ 29) 57 . } 58 }
我們可以看出ast還解析出了函數的範圍,以及範圍對應的對象。
小結
Go將所有可以識別的token抽象成Node,通過interface方式組織在一起,它們之間的關係如示意: