這篇文章是來自最新 justforfunc 中同標題的一段。這個程序的代碼可以在 justforfunc 倉庫 中找到。
問題陳述
想象一下,對于下面的代碼段,你如何將其中所有的標識符都提取出來。
package main import "fmt" func main() { fmt.Println("Hello, world") }
我們期望可以得到一個包含 main, fmt 和 Println 的列表。
標識符到底是什么?
為了回答這個問題, 我們需要了解一下有關計算機語言的理論知識。 但只要一點就足夠了,不用擔心有多復雜。
計算機語言,是由一系列有效的規則組成的。比如下面這個規則:
IfStmt = "if" [ SimpleStmt ";" ] Expression Block [ "else" ( IfStmt | Block ) ] .
上面這個規則告訴我們 if 語句在 Go 語言中的樣子。“if”, “;”, 和 “else” 是幫助我們理解程序結構的關鍵詞。與此同時,還有 Expression Block, SimpleStmt 之類的其他規則。
這些規則組成的集合就是語法,你可以在 Go 語言規范中找到它們的詳細定義。
這些規則不是簡單的由程序的單個字符定義的,而是有一系列 token 組成。 這些token除了像 if 和 else 這樣的原子 token 外, 還有像整數 42,浮點數 4.2 和字符串 “hello” 這樣的復合 token, 以及像 main 這樣的標識符。
但是,我們是怎么知道 main 是一個標識符,而不是一個數字呢? 原來它也是有專門的規則來定義的。如果你讀過 Go 語言規范中的標識符部分,你就會發現如下的規則:
identifier = letter { letter | unicode_digit } .
在這條規則中,letter 和 unicode_digit 不是 token 而是字符。 所以有了這些規則,就可以寫一個程序來逐個字符地分析,一旦檢測到一組字符匹配到某一條規則,就 “發射”(emits) 出一個 token。
所以,如果我們以 fmt.Println 為例, 它可以產生這些 token:標識符 fmt, “.”, 以及標識符 Println。 這是一個函數調用嗎? 在這里我們還無法確定,而且我們也不關心。它的結構就是一個序列,表明 token 出現的順序。

這種能夠將給定的字符序列生成 token 序列的程序被稱為掃描器。Go 標準庫中的 go/scanner 就自帶一個掃描器。它生成的記號定義在 go/token 里。
使用 go/scanner
我們已經了解了什么是掃描器,那它如何使用呢?
從命令行中讀取參數
讓我們先從一個簡單程序開始,將傳給它的參數打印出來:
package main import ( "fmt" "os" ) func main() { if len(os.Args) < 2 { fmt.Fprintf(os.Stderr, "usage:nt%s [files]n", os.Args[0]) os.Exit(1) } for _, arg := range os.Args[1:] { fmt.Println(arg) } }
接下來,我們需要掃描從參數傳進來的文件:需要先創建一個新的掃描器,然后用文件的內容來初始化。
打印每個 token
在我們調用 scanner.Scanner 的 Init 方法之前,需要先讀取文件內容,然后為每個掃描過的文件創建一個 token.FileSet 以便來保存 token.File。
掃描器一經初始化,我們就能調用其 Scan 方法來打印 token。 一旦我們得到一個 EOF(End Of File) token,就說明達到文件末尾了。
fs := token.NewFileSet() for _, arg := range os.Args[1:] { b, err := ioutil.ReadFile(arg) if err != nil { log.Fatal(err) } f := fs.AddFile(arg, fs.Base(), len(b)) var s scanner.Scanner s.Init(f, b, nil, scanner.ScanComments) for { _, tok, lit := s.Scan() if tok == token.EOF { break } fmt.Println(tok, lit) } }
統計 token
太棒了,我們已經能夠打印出所有的 token 了,但是我們還需要跟蹤每個標識符出現的次數,然后按照出現次數排序,并打印出前 5 位。
在 Go 中,實現以上需求的最好的方法是用一個 map,讓標識符來做 key, 其出現次數做 value。
每當一個標識符出現一次,計數器就加一。最后,我們將 map 轉換為一個能夠排序和打印的數組。
counts := make(map[string]int) // [code removed for clarity] for { _, tok, lit := s.Scan() if tok == token.EOF { break } if tok == token.IDENT { counts[lit]++ } } // [為了閱讀清晰,移除部分代碼] type pair struct { s string n int } pairs := make([]pair, 0, len(counts)) for s, n := range counts { pairs = Append(pairs, pair{s, n})rm -f } sort.Slice(pairs, func(i, j int) bool { return pairs[i].n > pairs[j].n }) for i := 0; i < len(pairs) && i < 5; i++ { fmt.Printf("%6d %sn", pairs[i].n, pairs[i].s) }
為了不影響理解,有些代碼被刪除了。你可以在這里獲取完整的源碼。
哪些是最常用的標識符?
我們來用這個程序分析一下 github.com/golang/go 上的代碼:
$ go install github.com/campoy/justforfunc/24-ast/scanner $ scanner ~/go/src/**/*.go 82163 v 46584 err 44681 Args 43371 t 37717 x
在短標識符里,最常用的標識符是字母 v 。那我們修改下代碼來計算一些長標識符:
for s, n := range counts { if len(s) >= 3 { pairs = append(pairs, pair{s, n}) } }
再來一次:
$ go install github.com/campoy/justforfunc/24-ast/scanner $ scanner ~/go/src/**/*.go 46584 err 44681 Args 36738 nil 25761 true 21723 AddArg
果不其然,err 和 nil 是最常見的標識符,畢竟每個程序中都有 if err != nil 這樣的語句。 但 Args 出現頻度這么高怎么回事?
欲知詳情如何,且聽下回分解。
via: https://medium.com/@francesc/whats-the-most-common-identifier-in-go-s-stdlib-e468f3c9c7d9
作者:Francesc Campoy 譯者:kaneg 校對:polaris1119