Go 语言标准库中最常用的标识符是什么?


Go 语言标准库中最常用的标识符是什么?

文章插图
 
这篇文章是来自最新 justforfunc 中同标题的一段 。这个程序的代码可以在 justforfunc 仓库 中找到 。
问题陈述想象一下 , 对于下面的代码段 , 你如何将其中所有的标识符都提取出来 。
package 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 出现的顺序 。
Go 语言标准库中最常用的标识符是什么?

文章插图
 
这种能够将给定的字符序列生成 token 序列的程序被称为扫描器 。Go 标准库中的 go/scanner 就自带一个扫描器 。它生成的记号定义在 go/token 里 。
使用 go/scanner我们已经了解了什么是扫描器 , 那它如何使用呢?
从命令行中读取参数让我们先从一个简单程序开始 , 将传给它的参数打印出来:
package mainimport ( "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 转换为一个能够排序和打印的数组 。


推荐阅读