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

這種能夠?qū)⒔o定的字符序列生成 token 序列的程序被稱為掃描器。Go 標(biāo)準(zhǔn)庫中的 go/scanner 就自帶一個(gè)掃描器。它生成的記號(hào)定義在 go/token 里。
使用 go/scanner
我們已經(jīng)了解了什么是掃描器,那它如何使用呢?
從命令行中讀取參數(shù)
讓我們先從一個(gè)簡(jiǎn)單程序開始,將傳給它的參數(shù)打印出來:
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) } }
接下來,我們需要掃描從參數(shù)傳進(jìn)來的文件:需要先創(chuàng)建一個(gè)新的掃描器,然后用文件的內(nèi)容來初始化。
打印每個(gè) token
在我們調(diào)用 scanner.Scanner 的 Init 方法之前,需要先讀取文件內(nèi)容,然后為每個(gè)掃描過的文件創(chuàng)建一個(gè) token.FileSet 以便來保存 token.File。
掃描器一經(jīng)初始化,我們就能調(diào)用其 Scan 方法來打印 token。 一旦我們得到一個(gè) EOF(End Of File) token,就說明達(dá)到文件末尾了。
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) } }
統(tǒng)計(jì) token
太棒了,我們已經(jīng)能夠打印出所有的 token 了,但是我們還需要跟蹤每個(gè)標(biāo)識(shí)符出現(xiàn)的次數(shù),然后按照出現(xiàn)次數(shù)排序,并打印出前 5 位。
在 Go 中,實(shí)現(xiàn)以上需求的最好的方法是用一個(gè) map,讓標(biāo)識(shí)符來做 key, 其出現(xiàn)次數(shù)做 value。
每當(dāng)一個(gè)標(biāo)識(shí)符出現(xiàn)一次,計(jì)數(shù)器就加一。最后,我們將 map 轉(zhuǎn)換為一個(gè)能夠排序和打印的數(shù)組。
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) }
為了不影響理解,有些代碼被刪除了。你可以在這里獲取完整的源碼。
哪些是最常用的標(biāo)識(shí)符?
我們來用這個(gè)程序分析一下 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
在短標(biāo)識(shí)符里,最常用的標(biāo)識(shí)符是字母 v 。那我們修改下代碼來計(jì)算一些長(zhǎng)標(biāo)識(shí)符:
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 是最常見的標(biāo)識(shí)符,畢竟每個(gè)程序中都有 if err != nil 這樣的語句。 但 Args 出現(xiàn)頻度這么高怎么回事?
欲知詳情如何,且聽下回分解。
via: https://medium.com/@francesc/whats-the-most-common-identifier-in-go-s-stdlib-e468f3c9c7d9
作者:Francesc Campoy 譯者:kaneg 校對(duì):polaris1119