通過 go/parser 理解 Go

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。這篇文章所講內容和 [episode 25 of justforfunc](https://www.youtube.com/watch?v=YRWCa84pykM) 是相同的。## justforfunc 前情提要我們在[上一篇文章](https://studygolang.com/articles/12324)中使用 `go/scanner` 找出了標準庫中最常用的標識符。> 這個標識符就是 v為了能擷取到更有價值的資訊,我們只考慮大於等於三個字元的標識符。不出所料,在 Go 中最具代表性的判斷語句 `if err != nil {}` 中的 err 和 nil 出現的最為頻繁。## 全域變數和局部變數如果我們想要知道最常用的局部變數名應該怎麼做?如果想知道最常用的類型或函數呢?針對這些問題 go/scanner 並不能滿足我們的需求,因為它缺少對內容相關的支援。按前文的方法我們可以找到需要的 token(例:var a = 3),為了擷取 token 所在的範圍(包級,函數級,代碼塊級)我們需要內容相關的支援。在一個包中可以有很多的聲明,其中的一些可能是函式宣告,而在函式宣告中,又可能有局部變數、常量或函式宣告。但是我們如何在 token 序列中找到這種結構呢?每種程式設計語言都有從 token 序列到文法樹結構的轉換規則。就像下面這樣:```VarDecl = "var" ( VarSpec | "(" { VarSpec ";" } ")" ) .VarSpec = IdentifierList ( Type [ "=" ExpressionList ] | "=" ExpressionList ) .```這個轉換規則告訴我們一個 `VarDecl`(變數聲明) 以一個 `var` token 開始,緊接著是一個 `VarSpec`(變數說明)或是一個被括弧包圍的以分號分隔的標識符列表。注意:分號其實是 Go scanner 自動添加的,所以你不會在文法分析的時候看到他們。以 var a = 3 為例,使用 go/scanner 我們會得到這樣的 token:```[VAR],[IDENT "a"],[ASSIGN],[INT "3"],[SEMICOLON]```依據前文描述的規則,這是一個只有 VarSpec 的 VarDecl。緊接著我們分析出標識符列表(`IdentifierList`)裡有一個標識符(`Identifier`) `a`,沒有類型(`Type`),運算式列表(`ExpressionList`)有一個整數 3 的運算式(`Expression`)。如果用樹表示,會是下面圖片這樣:![image](https://raw.githubusercontent.com/studygolang/gctt-images/master/go-parser/0_STJNoHjXJBsnWB4x.png)這個能使我們能從 token 序列解析樹結構的規則叫做文法或句法,而解析出的樹結構叫做抽象文法樹,簡稱 AST。## 使用 go/scanner現在我們有足夠的理論基礎來寫一些代碼。來看看我們如何解析運算式 `var a = 3` 並且獲得他的 AST。```gopackage mainimport ("fmt""go/parser""go/token""log")func main() {fs := token.NewFileSet()f, err := parser.ParseFile(fs, "", "var a = 3", parser.AllErrors)if err != nil {log.Fatal(err)}fmt.Println(f)}```這段代碼可以編譯通過但是在運行時會報錯:```1:1: expected 'package', found 'var' (and 1 more errors)```為瞭解析這個我們叫做 `ParseFile` 的聲明,我們需要給出一個完整的 go 源檔案格式(以 package 作為源檔案開頭)。> 注意:注釋可以寫在 package 前面如果你正在解析一個形如 `3 + 5` 的運算式或者其他可以看作一個值的代碼你可以將它們看作一個參數叫做 ParseExpr。但是在函式宣告時不能這麼做。添加 `package main` 到代碼的開頭並查看我們獲得的 AST 樹。```gopackage mainimport ("fmt""go/parser""go/token""log")func main() {fs := token.NewFileSet()f, err := parser.ParseFile(fs, "", "package main; var a = 3", parser.AllErrors)if err != nil {log.Fatal(err)}fmt.Println(f)}```運行後輸出如下:```$ go run main.go&{<nil> 1 main [0xc420054100] scope 0xc42000e210 {var a} [] [] []}```將 `Println` 換成 `fmt.Printf("%#v",f)` 並重試:```go run main.go&ast.File{Doc:(*ast.CommentGroup)(nil), Package:1, Name:(*ast.Ident)(0xc42000a060), Decls:[]ast.Decl{(*ast.GenDecl)(0xc420054100)}, Scope:(*ast.Scope)(0xc42000e210), Imports:[]*ast.ImportSpec(nil), Unresolved:[]*ast.Ident(nil), Comments:[]*ast.CommentGroup(nil)}```看起來可以了但是不易讀,可以使用 `github.com/davecgh/go-spew/spew` 來讓輸出更易讀:```gopackage mainimport ("go/parser""go/token""log" "github.com/davecgh/go-spew/spew")func main() {fs := token.NewFileSet()f, err := parser.ParseFile(fs, "", "package main; var a = 3", parser.AllErrors)if err != nil {log.Fatal(err)}spew.Dump(f)}```重新運行程式我們會得到更加易讀的輸出:```$ go run main.go(*ast.File)(0xc42009c000)({ Doc: (*ast.CommentGroup)(<nil>), Package: (token.Pos) 1, Name: (*ast.Ident)(0xc42000a120)(main), Decls: ([]ast.Decl) (len=1 cap=1) { (*ast.GenDecl)(0xc420054100)({ Doc: (*ast.CommentGroup)(<nil>), TokPos: (token.Pos) 15, Tok: (token.Token) var, Lparen: (token.Pos) 0, Specs: ([]ast.Spec) (len=1 cap=1) {(*ast.ValueSpec)(0xc4200802d0)({ Doc: (*ast.CommentGroup)(<nil>), Names: ([]*ast.Ident) (len=1 cap=1) { (*ast.Ident)(0xc42000a140)(a) }, Type: (ast.Expr) <nil>, Values: ([]ast.Expr) (len=1 cap=1) { (*ast.BasicLit)(0xc42000a160)({ ValuePos: (token.Pos) 23, Kind: (token.Token) INT, Value: (string) (len=1) "3" }) }, Comment: (*ast.CommentGroup)(<nil>)}) }, Rparen: (token.Pos) 0 }) }, Scope: (*ast.Scope)(0xc42000e2b0)(scope 0xc42000e2b0 {var a}), Imports: ([]*ast.ImportSpec) <nil>, Unresolved: ([]*ast.Ident) <nil>, Comments: ([]*ast.CommentGroup) <nil>})```我推薦花點時間認真看一下這個樹,並且找到他們對應的源碼部分。`Scope`,`Obj`,`Unresolved` 我們會在下面的章節說。## 從 AST 到代碼有的時候以源碼的位置 列印 AST 比樹結構更清晰。使用 go/printer 可以非常簡單的列印源碼儲存的 AST 資訊。```gopackage mainimport ("go/parser""go/printer""go/token""log""os")func main() {fs := token.NewFileSet()f, err := parser.ParseFile(fs, "", "package main; var a = 3", parser.AllErrors)if err != nil {log.Fatal(err)}printer.Fprint(os.Stdout, fs, f)}```執行這段代碼會列印我們源碼的解析結果,將 parser.AllErrors 替換成 parser.ImportsOnly 或者其它值會有不同的輸出結果。## AST 指南AST 樹有我們想知道的所有資訊,但是如何才能找出我們想要的資訊呢?這時 go/ast 包就派上了用場。我們使用 ast.Walk。這個函數接受 2 個參數。第二個參數是一個 ast.Node,AST 中所有節點都實現了的介面。第一個參數是 ast.Visitor 介面。這個介面有一個方法:```gotype Visitor interface {Visit(node Node) (w Visitor)}```現在我們已經有了一個節點,是 `parser.ParseFile` 返回的 `ast.File`。但是我們需要建立一個自己的 `ast.Visitor`。我們實現了一個列印節點類型並返回自己的 `ast.Visitor`。```gopackage mainimport ("fmt""go/ast""go/parser""go/token""log")func main() {fs := token.NewFileSet()f, err := parser.ParseFile(fs, "", "package main; var a = 3", parser.AllErrors)if err != nil {log.Fatal(err)}var v visitorast.Walk(v, f)}type visitor struct{}func (v visitor) Visit(n ast.Node) ast.Visitor {fmt.Printf("%T\n", n)return v}```運行這個程式我們會得到沒有樹結構的節點序列。那些 nil 節點是什嗎?在 ast.Walk 的文檔中可以瞭解我們返回 visitor 的時候會繼續找他的下級節點,如果沒有下級節點將會返回 nil。知道這個特性後我們就可以像樹那樣列印這個結果。```gotype visitor intfunc (v visitor) Visit(n ast.Node) ast.Visitor {if n == nil {return nil}fmt.Printf("%s%T\n", strings.Repeat("\t", int(v)), n)return v + 1}```程式的其他部分沒有改變,執行以後我們會得到以下輸出:```*ast.File*ast.Ident*ast.GenDecl*ast.ValueSpec*ast.Ident*ast.BasicLit```## 每種標識符最常用的名稱都是什嗎?我們已經能夠解析代碼並訪問 AST 節點從而匯出我們想要的資訊:哪個變數名是包中最常用的。代碼和以前很像都是使用 go/scanner 從命令列讀取檔案清單。```gopackage mainimport ("fmt""go/ast""go/parser""go/token""log""os""strings")func main() {if len(os.Args) < 2 {fmt.Fprintf(os.Stderr, "usage:\n\t%s [files]\n", os.Args[0])os.Exit(1)}fs := token.NewFileSet()var v visitorfor _, arg := range os.Args[1:] {f, err := parser.ParseFile(fs, arg, nil, parser.AllErrors)if err != nil {log.Printf("could not parse %s: %v", arg, err)continue}ast.Walk(v, f)}}type visitor intfunc (v visitor) Visit(n ast.Node) ast.Visitor {if n == nil {return nil}fmt.Printf("%s%T\n", strings.Repeat("\t", int(v)), n)return v + 1}```執行這段代碼我們將會得到所有來自命令列參數的檔案的 AST。我們可以試試傳入剛剛寫的 main.go 檔案。```$ go build -o parser main.go && parser main.go# output removed for brevity```改變 visitor 來跟蹤每種標識符都被不同的變數聲明形式使用了多少次。首先我們來跟蹤短變數聲明。因為我們知道它一般都是一個局部變數。```gotype visitor struct {locals map[string]int}func (v visitor) Visit(n ast.Node) ast.Visitor {if n == nil {return nil}switch d := n.(type) {case *ast.AssignStmt:for _, name := range d.Lhs {if ident, ok := name.(*ast.Ident); ok {if ident.Name == "_" {continue}if ident.Obj != nil && ident.Obj.Pos() == ident.Pos() {v.locals[ident.Name]++}}}}return v}```檢查每個指派陳述式的名字是不是需要被忽略的 `_` ,這時我們需要 `Obj` 欄位跟蹤聲明的上下文。如果 `Obj` 的欄位是 nil,說明這個變數不在本檔案中定義,所以它不是一個局部變數聲明我們可以忽略它。如果我們對標準庫執行這段代碼將會得到:```7761 err6310 x5446 got4702 i3821 c```有趣的是為什麼 v 不在,我們漏掉了什麼局部變數的聲明的方式嗎?## 考慮參數和 range 中的變數我們漏掉了一對節點類型其實它們也是一種局部變數。- 函數參數,接收者,傳回值名稱- range 語句因為會大部分沿用之前的代碼,所以我們特意為其定義了一個方法。```gofunc (v visitor) local(n ast.Node) {ident, ok := n.(*ast.Ident)if !ok {return}if ident.Name == "_" || ident.Name == "" {return}if ident.Obj != nil && ident.Obj.Pos() == ident.Pos() {v.locals[ident.Name]++}}```對於參數、傳回值和方法接收者,我們都會擷取到一個長度為一的標識符列表。再定義一個方法來處理這個標識符列表:```gofunc (v visitor) localList(fs []*ast.Field) {for _, f := range fs {for _, name := range f.Names {v.local(name)}}}```這樣我們就可以處理所有聲明局部變數的類型:```gocase *ast.AssignStmt:if d.Tok != token.DEFINE {return v}for _, name := range d.Lhs {v.local(name)}case *ast.RangeStmt:v.local(d.Key)v.local(d.Value)case *ast.FuncDecl:v.localList(d.Recv.List)v.localList(d.Type.Params.List)if d.Type.Results != nil {v.localList(d.Type.Results.List)}```現在讓我們運行這段代碼:```shell$ ./parser ~/go/src/**/*.gomost common local variable names 12264 err 9395 t 9163 x 7442 i 6127 c```## 處理 var 聲明現在我們需要進一步處理 var 聲明,它有可能是全域變數也有可能是局部變數,並且只有判斷其是否為 ast.File 級來判斷它是不是全域變數。為了達到這個目的我們為每個新的檔案建立 visitor 用來追蹤檔案中的全域變數,這樣我們就可以正確的計算出標識符的數量。我們會在結構體中增加一個 pkgDecls 類型為 map[*ast.GenDecl]bool。在我們的 visitor 中,我們會使用 newVisitor 函數建立一個新的 visitor 並進行初始化工作,而且還會添加 globals 欄位來跟蹤全域變數標識符被聲明的次數。```gotype visitor struct {pkgDecls map[*ast.GenDecl]boolglobals map[string]intlocals map[string]int}func newVisitor(f *ast.File) visitor {decls := make(map[*ast.GenDecl]bool)for _, decl := range f.Decls {if d, ok := decl.(*ast.GenDecl); ok {decls[d] = true}}return visitor{decls,make(map[string]int),make(map[string]int),}}```我們的 main 函數將會需要為每個檔案建立一個新的 visitor 去跟蹤匯總結果:```golocals, globals := make(map[string]int), make(map[string]int)for _, arg := range os.Args[1:] {f, err := parser.ParseFile(fs, arg, nil, parser.AllErrors)if err != nil {og.Printf("could not parse %s: %v", arg, err)continue}v := newVisitor(f)ast.Walk(v, f)for k, v := range v.locals {locals[k] += v}for k, v := range v.globals {globals[k] += v}}```還有最後一個部分需要完成就是需要跟蹤 *ast.GenDecl 節點並找到在變數中的所有聲明:```gocase *ast.GenDecl:if d.Tok != token.VAR {return v}for _, spec := range d.Specs {if value, ok := spec.(*ast.ValueSpec); ok {for _, name := range value.Names {if name.Name == "_" {continue}if v.pkgDecls[d] {v.globals[name.Name]++} else {v.locals[name.Name]++}}}}```在每個聲明中我們都只計算以 `token.VAR` 開頭的聲明。因此常量、類型和其他形式的標識符都會被忽略。在每個聲明中我們還要判斷它是全域變數還是局部變數,並相應的記錄出現次數並忽略 `_`。程式的完全版在 [這裡](https://github.com/campoy/justforfunc/blob/master/25-go-parser/main.go),執行程式我們會得到:```shell$ ./parser ~/go/src/**/*.gomost common local variable names 12565 err 9876 x 9464 t 7554 i 6226 bmost common global variable names29 errors28 signals23 failed15 tests12 debug```至此,我們得出結論,最常用的局部變數就是 err。最常用的包名是 errors。哪個常量名字最常用?我們如何找到他們?## 感謝如果你喜歡這篇文章可以分享它也可以訂閱我們的頻道,或者在關注我。也可以考慮成為一個贊助者。

via: https://medium.com/@francesc/understanding-go-programs-with-go-parser-c4e88a6edb87

作者:JohnKoepi 譯者:saberuster 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

1113 次點擊  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.