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

zooeymoon · · 1133 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

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

有疑问加站长微信联系(非本文作者)

本文来自:Segmentfault

感谢作者:zooeymoon

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

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1133 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传