這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。這篇文章是來自最新 [justforfunc](http://justforfunc.com/) 中同標題的一段。這個程式的[代碼](https://github.com/campoy/justforfunc/blob/master/24-go-scanner/main.go)可以在 [justforfunc 倉庫](https://github.com/campoy/justforfunc) 中找到。### 問題陳述想象一下,對於下面的程式碼片段,你如何將其中所有的標識符都提取出來。```gopackage mainimport "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我們已經瞭解了什麼是掃描器,那它如何使用呢?#### 從命令列中讀取參數讓我們先從一個簡單程式開始,將傳給它的參數列印出來:```gopackage mainimport ("fmt""os")func main() {if len(os.Args) < 2 {fmt.Fprintf(os.Stderr, "usage:\n\t%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,就說明達到檔案末尾了。```gofs := 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.Scanners.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 轉換為一個能夠排序和列印的數組。```gocounts := 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 stringn 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 %s\n", pairs[i].n, pairs[i].s)}```為了不影響理解,有些代碼被刪除了。你可以在[這裡](https://github.com/campoy/justforfunc/blob/master/24-go-scanner/main.go)擷取完整的源碼。### 哪些是最常用的標識符?我們來用這個程式分析一下 github.com/golang/go 上的代碼:```bash$ go install github.com/campoy/justforfunc/24-ast/scanner$ scanner ~/go/src/**/*.go 82163 v 46584 err 44681 Args 43371 t 37717 x```在短標識符裡,最常用的標識符是字母 **v** 。那我們修改下代碼來計算一些長標識符:```gofor s, n := range counts {if len(s) >= 3 {pairs = append(pairs, pair{s, n})}}```再來一次:```bash$ 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
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
504 次點擊