使用go/scanner库来查找golang标准库源码中最常用的标识符

在YouTube上又看了Francesc Campoy大神的视频。这一期他主要讲了go/scanner库的用法。那么什么是go/scanner呢?

官方的定义是:

Package scanner implements a scanner for Go source text. It takes a []byte as source which can then be tokenized through repeated calls to the Scan method.

翻译过来就是:

scanner包实现了一个对于go源码文本的扫描器,它把[]byte作为一个源,通过重复的调用Scan方法来进行标记

以下是官方给的例子:

package main

import (
    "fmt"
    "go/scanner"
    "go/token"
)

func main() {
    // src is the input that we want to tokenize.
    src := []byte("cos(x) + 1i*sin(x) // Euler")

    // Initialize the scanner.
    var s scanner.Scanner
    fset := token.NewFileSet()                      // positions are relative to fset
    file := fset.AddFile("", fset.Base(), len(src)) // register input "file"
    s.Init(file, src, nil /* no error handler */, scanner.ScanComments)

    // Repeated calls to Scan yield the token sequence found in the input.
    for {
        pos, tok, lit := s.Scan()
        if tok == token.EOF {
            break
        }
        fmt.Printf("%s\t%s\t%q\n", fset.Position(pos), tok, lit)
    }

}

这里有几点需要说明

  • var s scanner.Scanner为扫描器
  • fset := token.NewFileSet()为创建一个新的文件集
  • s.Init(file, src, nil /* no error handler */, scanner.ScanComments) 初始化一个扫描器
  • pos, tok, lit := s.Scan() 返回的三个参数分别为:标识符位置,标识符文字字符串。需要有一点说明:如果标识符为token.EOF,则说明扫描结束

现在我们开始编写代码:

1.首先我们需要保证运行的参数大于等于两个:

package main

import (
    "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)
    }

}

2.之后开始循环os.Args把从第二个开始的所有参数遍历出来

package main

import (
    "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)

    }

}

3.接下来我们开始要使用我们的go/scanner库了,这里我们要依次完成

  • 文件集创建
  • ioutil读取文件
  • 把文件加入到文件集
  • 初始化扫描器
  • 循环迭代,如果标识符为token.EOF,则跳出循环
  • 打印输出
package main

import (
    "fmt"
    "go/scanner"
    "go/token"
    "io/ioutil"
    "log"
    "os"
)

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()

    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 {
            pos , tok , lit := s.Scan()
            if tok == token.EOF {
                break
            }

            fmt.Println(pos , tok , lit)
        }

    }

}

我们运行代码产生的结果如下:

➜  scanner git:(master) ✗ ./scanner stdlib.go
1 package package
9 IDENT main
13 ; 

15 import import
22 ( 
25 STRING "fmt"
30 ; 

32 STRING "go/scanner"
44 ; 

46 STRING "go/token"
56 ; 

58 STRING "io/ioutil"
69 ; 

71 STRING "log"
76 ; 

78 STRING "os"
82 ; 

83 ) 
84 ; 

86 func func
91 IDENT main
95 ( 
96 ) 
98 { 
101 if if
104 IDENT len
107 ( 
108 IDENT os
110 . 
111 IDENT Args
115 ) 
117 < 
119 INT 2
121 { 
125 IDENT fmt
128 . 
129 IDENT Fprintf
136 ( 
137 IDENT os
139 . 
140 IDENT Stderr
147 , 
149 STRING "usage:\n\t%s [files] \n"
174 , 
175 IDENT os
177 . 
178 IDENT Args
182 [ 
183 INT 0
184 ] 
185 ) 
186 ; 

189 IDENT os
191 . 
192 IDENT Exit
196 ( 
197 INT 1
198 ) 
199 ; 

201 } 
202 ; 

205 IDENT fs
208 := 
211 IDENT token
216 . 
217 IDENT NewFileSet
227 ( 
228 ) 
229 ; 

232 for for
236 IDENT _
238 , 
240 IDENT arg
244 := 
247 range range
253 IDENT os
255 . 
256 IDENT Args
260 [ 
261 INT 1
262 : 
263 ] 
265 { 
270 IDENT b
272 , 
274 IDENT err
278 := 
281 IDENT ioutil
287 . 
288 IDENT ReadFile
296 ( 
297 IDENT arg
300 ) 
301 ; 

304 if if
307 IDENT err
311 != 
314 IDENT nil
318 { 
323 IDENT log
326 . 
327 IDENT Fatal
332 ( 
333 IDENT err
336 ) 
337 ; 

340 } 
341 ; 

344 IDENT f
346 := 
349 IDENT fs
351 . 
352 IDENT AddFile
359 ( 
360 IDENT arg
363 , 
364 IDENT fs
366 . 
367 IDENT Base
371 ( 
372 ) 
373 , 
374 IDENT len
377 ( 
378 IDENT b
379 ) 
380 ) 
381 ; 

384 var var
388 IDENT s
390 IDENT scanner
397 . 
398 IDENT Scanner
405 ; 

408 IDENT s
409 . 
410 IDENT Init
414 ( 
415 IDENT f
417 , 
418 IDENT b
419 , 
420 IDENT nil
423 , 
424 IDENT scanner
431 . 
432 IDENT ScanComments
444 ) 
445 ; 

449 for for
453 { 
458 IDENT pos
462 , 
464 IDENT tok
468 , 
470 IDENT lit
474 := 
477 IDENT s
478 . 
479 IDENT Scan
483 ( 
484 ) 
485 ; 

489 if if
492 IDENT tok
496 == 
499 IDENT token
504 . 
505 IDENT EOF
509 { 
515 break break
520 ; 

524 } 
525 ; 

530 IDENT fmt
533 . 
534 IDENT Println
541 ( 
542 IDENT pos
546 , 
548 IDENT tok
552 , 
554 IDENT lit
557 ) 
558 ; 

561 } 
562 ; 

565 } 
566 ; 

568 } 
569 ; 

这里有几点我们可以发现

  • 虽然我们没有使用;符号,但是源码自动给我们添加上了,所以说有些ide不推荐我们在换行的时候使用分号。
  • 如果标识符是关键字,那么lit参数回打印出来,比如说125 IDENT fmt

3.现在我们有了我们想要的,所以我们现在需要修正代码,把出现最多的标识符字符串打印出来,这里我们需要通过以下步骤实现

  • 创建一个map映射
  • 在循环中如果发现token为标识符,则放入映射中
  • 定义一个pair结构体
  • 创建一个pair结构体切片
  • 把map映射的元素放入到切片中
  • 排序
  • 输出

代码如下:

package main

import (
    "fmt"
    "go/scanner"
    "go/token"
    "io/ioutil"
    "log"
    "os"
    "sort"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Fprintf(os.Stderr , "usage:\n\t%s [files] \n",os.Args[0])
        os.Exit(1)
    }

    counts := map[string]int{}

    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
            }

            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})
    }

    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("%5d %s\n",pairs[i].n , pairs[i].s)
    }
}

输出的结果为:

    9 pairs
    8 i
    7 s
    6 n
    5 os

我们感觉有一些字符有些短,没有意义,我们需要过滤统计的字符,让其大于3。
这里我们修改以下循环counts的条件

for s ,n := range counts {

        if len(s) > 3 {
            pairs = append(pairs , pair{s,n})
        }
    }

我们重新运行一次代码,产生的结果如下:

    9 pairs
    4 counts
    3 Args
    3 pair
    3 token

代码测试通过,接下来我们需要把go run后面的参数修改以下,让其可以扫描整个golang的标准库

52720 Args
32277 uintptr
31416 AddArg
27414 true
25265 string

我们发现标准库里面用的最多的是Args

这里我们有个小插曲发现,如果绝对路径定义要扫描的文件路径

➜  scanner git:(master) ✗ ./scanner /usr/local/Cellar/go/1.14/libexec/src/**/*.go
zsh: argument list too long: ./scanner

发现参数过长,所以我把二进制文件放入到了/usr/local/Cellar/go/1.14/文件夹下
之后使用命令:

./scanner ./libexec/src/**/*.go
➜  1.14 ./scanner ./libexec/src/**/*.go
52720 Args
32277 uintptr
31416 AddArg
27414 true
25265 string

完美!!!!!

最终的代码为:

package main

import (
    "fmt"
    "go/scanner"
    "go/token"
    "io/ioutil"
    "log"
    "os"
    "sort"
)

func main() {
    if len(os.Args) < 2 {
        fmt.Fprintf(os.Stderr , "usage:\n\t%s [files] \n",os.Args[0])
        os.Exit(1)
    }

    counts := map[string]int{}

    fs := token.NewFileSet()

    for _ , arg := range os.Args[1:] {

        b , err := ioutil.ReadFile(arg)
        if err != nil {
            log.Fatal("haha",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
            }

            if tok == token.IDENT {
                counts[lit]++
            }

        }

    }

    type pair struct {
        s string
        n int
    }

    pairs := make([]pair , 0 , len(counts))
    for s ,n := range counts {

        if len(s) > 3 {
            pairs = append(pairs , pair{s,n})
        }
    }

    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("%5d %s\n",pairs[i].n , pairs[i].s)
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容