我们拿到一个 golang 的工程后(通常是个微服务),怎么从词法、语法的角度来分析源代码呢?golang 提供了一系列的工具供我们使用:

  • go/scanner 包提供词法分析功能,将源代码转换为一系列的 token,以供 go/parser 使用
  • go/parser 包提供语法分析功能,将这些 token 转换为 AST(Abstract Syntax Tree, 抽象语法树)

Scanner

  • 任何编译器所做的第一步都是将源代码转换成 token,这就是 Scanner 所做的事
  • token 可以是关键字,字符串值,变量名以及函数名等等
  • 在 golang 中,每个 token 都以它所处的位置,类型和原始字面量来表示

比如我们用如下代码扫描源代码的 token:

  1. func TestScanner(t *testing.T) {
  2. src := []byte(`package main
  3. import "fmt"
  4. //comment
  5. func main() {
  6. fmt.Println("Hello, world!")
  7. }
  8. `)
  9. var s scanner.Scanner
  10. fset := token.NewFileSet()
  11. file := fset.AddFile("", fset.Base(), len(src))
  12. s.Init(file, src, nil, 0)
  13. for {
  14. pos, tok, lit := s.Scan()
  15. fmt.Printf("%-6s%-8s%q\n", fset.Position(pos), tok, lit)
  16. if tok == token.EOF {
  17. break
  18. }
  19. }
  20. }

结果:

  1. 1:1 package "package"
  2. 1:9 IDENT "main"
  3. 1:13 ; "\n"
  4. 2:1 import "import"
  5. 2:8 STRING "\"fmt\""
  6. 2:13 ; "\n"
  7. 4:1 func "func"
  8. 4:6 IDENT "main"
  9. 4:10 ( ""
  10. 4:11 ) ""
  11. 4:13 { ""
  12. 5:3 IDENT "fmt"
  13. 5:6 . ""
  14. 5:7 IDENT "Println"
  15. 5:14 ( ""
  16. 5:15 STRING "\"Hello, world!\""
  17. 5:30 ) ""
  18. 5:31 ; "\n"
  19. 6:1 } ""
  20. 6:2 ; "\n"
  21. 6:3 EOF ""

注意没有扫描出注释,需要的话要将s.Init的最后一个参数改为scanner.ScanComments

看下 go/token/token.go 的源代码可知,token 就是一堆定义好的枚举类型,对于每种类型的字面值都有对应的 token。

  1. // Copyright 2009 The Go Authors. All rights reserved.
  2. // Use of this source code is governed by a BSD-style
  3. // license that can be found in the LICENSE file.
  4. // Package token defines constants representing the lexical tokens of the Go
  5. // programming language and basic operations on tokens (printing, predicates).
  6. //
  7. package token
  8. import "strconv"
  9. // Token is the set of lexical tokens of the Go programming language.
  10. type Token int
  11. // The list of tokens.
  12. const (
  13. // Special tokens
  14. ILLEGAL Token = iota
  15. EOF
  16. COMMENT
  17. literal_beg
  18. // Identifiers and basic type literals
  19. // (these tokens stand for classes of literals)
  20. IDENT // main
  21. INT // 12345
  22. FLOAT // 123.45
  23. IMAG // 123.45i
  24. CHAR // 'a'
  25. STRING // "abc"
  26. literal_end
  27. ...略...
  28. )

Parser

  • 当源码被扫描成 token 之后,结果就被传递给了 Parser
  • 将 token 转换为抽象语法树(AST)
  • 编译时的错误也是在这个时候报告的

什么是 AST 呢,这篇文章何为语法树讲的很好。简单来说,AST(Abstract Syntax Tree)是使用树状结构表示源代码的语法结构,树的每一个节点就代表源代码中的一个结构。

来看如下的例子:

  1. func TestParserAST(t *testing.T) {
  2. src := []byte(`/*comment0*/
  3. package main
  4. import "fmt"
  5. //comment1
  6. /*comment2*/
  7. func main() {
  8. fmt.Println("Hello, world!")
  9. }
  10. `)
  11. // Create the AST by parsing src.
  12. fset := token.NewFileSet() // positions are relative to fset
  13. f, err := parser.ParseFile(fset, "", src, 0)
  14. if err != nil {
  15. panic(err)
  16. }
  17. // Print the AST.
  18. ast.Print(fset, f)
  19. }

结果很长就不贴出来了,整个 AST 的树形结构可以用如下图表示:

golang深入源代码系列之一:AST的遍历 - 图1

同样注意没有扫描出注释,需要的话要将parser.ParseFile的最后一个参数改为parser.ParseComments 再对照如下 ast.File 的定义:

  1. type File struct {
  2. Doc *CommentGroup // associated documentation; or nil
  3. Package token.Pos // position of "package" keyword
  4. Name *Ident // package name
  5. Decls []Decl // top-level declarations; or nil
  6. Scope *Scope // package scope (this file only)
  7. Imports []*ImportSpec // imports in this file
  8. Unresolved []*Ident // unresolved identifiers in this file
  9. Comments []*CommentGroup // list of all comments in the source file
  10. }

可知上述例子中的/*comment0*/对照结构中的 Doc,是整个 go 文件的描述。和//comment1 以及/*comment2*/不同,后两者是 Decls 中的结构。

遍历 AST

golang 提供了 ast.Inspect 方法供我们遍历整个 AST 树,比如如下例子遍历整个 example/test1.go 文件寻找所有 return 返回的地方:

  1. func TestInspectAST(t *testing.T) {
  2. // Create the AST by parsing src.
  3. fset := token.NewFileSet() // positions are relative to fset
  4. f, err := parser.ParseFile(fset, "./example/test1.go", nil, parser.ParseComments)
  5. if err != nil {
  6. panic(err)
  7. }
  8. ast.Inspect(f, func(n ast.Node) bool {
  9. // Find Return Statements
  10. ret, ok := n.(*ast.ReturnStmt)
  11. if ok {
  12. fmt.Printf("return statement found on line %v:\n", fset.Position(ret.Pos()))
  13. printer.Fprint(os.Stdout, fset, ret)
  14. fmt.Printf("\n")
  15. return true
  16. }
  17. return true
  18. })
  19. }

example/test1.go 代码如下:

  1. package main
  2. import "fmt"
  3. import "strings"
  4. func test1() {
  5. hello := "Hello"
  6. world := "World"
  7. words := []string{hello, world}
  8. SayHello(words)
  9. }
  10. // SayHello says Hello
  11. func SayHello(words []string) bool {
  12. fmt.Println(joinStrings(words))
  13. return true
  14. }
  15. // joinStrings joins strings
  16. func joinStrings(words []string) string {
  17. return strings.Join(words, ", ")
  18. }

结果为:

  1. return statement found on line ./example/test1.go:16:2:
  2. return true
  3. return statement found on line ./example/test1.go:21:2:
  4. return strings.Join(words, ", ")

还有另一种方法遍历 AST,构造一个 ast.Visitor 接口:

  1. type Visitor int
  2. func (v Visitor) Visit(n ast.Node) ast.Visitor {
  3. if n == nil {
  4. return nil
  5. }
  6. fmt.Printf("%s%T\n", strings.Repeat("\t", int(v)), n)
  7. return v + 1
  8. }
  9. func TestASTWalk(t *testing.T) {
  10. // Create the AST by parsing src.
  11. fset := token.NewFileSet() // positions are relative to fset
  12. f, err := parser.ParseFile(fset, "", "package main; var a = 3", parser.ParseComments)
  13. if err != nil {
  14. panic(err)
  15. }
  16. var v Visitor
  17. ast.Walk(v, f)
  18. }

旨在递归地打印出所有的 token 节点,输出:

  1. *ast.File
  2. *ast.Ident
  3. *ast.GenDecl
  4. *ast.ValueSpec
  5. *ast.Ident
  6. *ast.BasicLit

以上基础知识主要参考文章How a Go Program Compiles down to Machine Code(译文:Go 程序到机器码的编译之旅。下面来点干货。

其实翻一翻网上将这个 golang 的 ast 的文章也不少,但是大多停留在上文的阶段,没有实际指导开发运用。那么我们假设现在有一个任务,拿到了一个别人的项目(俗称接盘侠),现在需要找到源文件中的这些地方:特征是调用了context.WithCancel函数,并且入参为 nil。比如 example/test2.go 文件里面,有十多种可能:

  1. package main
  2. import (
  3. "context"
  4. "fmt"
  5. )
  6. func test2(a string, b int) {
  7. context.WithCancel(nil) //000
  8. if _, err := context.WithCancel(nil); err != nil { //111
  9. context.WithCancel(nil) //222
  10. } else {
  11. context.WithCancel(nil) //333
  12. }
  13. _, _ = context.WithCancel(nil) //444
  14. go context.WithCancel(nil) //555
  15. go func() {
  16. context.WithCancel(nil) //666
  17. }()
  18. defer context.WithCancel(nil) //777
  19. defer func() {
  20. context.WithCancel(nil) //888
  21. }()
  22. data := map[string]interface{}{
  23. "x2": context.WithValue(nil, "k", "v"), //999
  24. }
  25. fmt.Println(data)
  26. /*
  27. for i := context.WithCancel(nil); i; i = false {//aaa
  28. context.WithCancel(nil)//bbb
  29. }
  30. */
  31. var keys []string = []string{"ccc"}
  32. for _, k := range keys {
  33. fmt.Println(k)
  34. context.WithCancel(nil)
  35. }
  36. }

从 000 到 ccc,对应 golang 的 AST 的不同结构类型,现在需要把他们全部找出来。其中 bbb 这种情况代表了 for 语句,只不过在context.WithCancel函数不适用,所以注掉了。为了解决这个问题,首先需要仔细分析 go/ast 的 Node 接口。

AST 的结构定义

go/ast/ast.go 中指明了 ast 节点的定义:

  1. // All node types implement the Node interface.
  2. type Node interface {
  3. Pos() token.Pos // position of first character belonging to the node
  4. End() token.Pos // position of first character immediately after the node
  5. }
  6. // All expression nodes implement the Expr interface.
  7. type Expr interface {
  8. Node
  9. exprNode()
  10. }
  11. // All statement nodes implement the Stmt interface.
  12. type Stmt interface {
  13. Node
  14. stmtNode()
  15. }
  16. // All declaration nodes implement the Decl interface.
  17. type Decl interface {
  18. Node
  19. declNode()
  20. }

语法有三个主体:表达式 (expression)、语句 (statement)、声明 (declaration),Node 是基类,用于标记该节点的位置的开始和结束。而三个主体的函数没有实际意义,只是用三个 interface 来划分不同的语法单位, 如果某个语法是 Stmt 的话, 就实现一个空的 stmtNode 函数即可。参考这篇文章go-parser - 语法分析,定义了源文件中可能出现的语法结构。列表如下:

普通 Node, 不是特定语法结构, 属于某个语法结构的一部分.

  • Comment 表示一行注释 // 或者 / /
  • CommentGroup 表示多行注释
  • Field 表示结构体中的一个定义或者变量, 或者函数签名当中的参数或者返回值
  • FieldList 表示以”{}” 或者”()” 包围的 Filed 列表

Expression & Types (都划分成 Expr 接口)

  • BadExpr 用来表示错误表达式的占位符
  • Ident 比如报名, 函数名, 变量名
  • Ellipsis 省略号表达式, 比如参数列表的最后一个可以写成 arg…
  • BasicLit 基本字面值, 数字或者字符串
  • FuncLit 函数定义
  • CompositeLit 构造类型, 比如 {1,2,3,4}
  • ParenExpr 括号表达式, 被括号包裹的表达式
  • SelectorExpr 选择结构, 类似于 a.b 的结构
  • IndexExpr 下标结构, 类似这样的结构 expr[expr]
  • SliceExpr 切片表达式, 类似这样 expr[low:mid:high]
  • TypeAssertExpr 类型断言类似于 X.(type)
  • CallExpr 调用类型, 类似于 expr()
  • StarExpr 表达式, 类似于 X
  • UnaryExpr 一元表达式
  • BinaryExpr 二元表达式
  • KeyValueExp 键值表达式 key:value
  • ArrayType 数组类型
  • StructType 结构体类型
  • FuncType 函数类型
  • InterfaceType 接口类型
  • MapType map 类型
  • ChanType 管道类型

Statements

  • BadStmt 错误的语句
  • DeclStmt 在语句列表里的申明
  • EmptyStmt 空语句
  • LabeledStmt 标签语句类似于 indent:stmt
  • ExprStmt 包含单独的表达式语句
  • SendStmt chan 发送语句
  • IncDecStmt 自增或者自减语句
  • AssignStmt 赋值语句
  • GoStmt Go 语句
  • DeferStmt 延迟语句
  • ReturnStmt return 语句
  • BranchStmt 分支语句 例如 break continue
  • BlockStmt 块语句 {} 包裹
  • IfStmt If 语句
  • CaseClause case 语句
  • SwitchStmt switch 语句
  • TypeSwitchStmt 类型 switch 语句 switch x:=y.(type)
  • CommClause 发送或者接受的 case 语句, 类似于 case x <-:
  • SelectStmt select 语句
  • ForStmt for 语句
  • RangeStmt range 语句

Declarations

  • Spec type
    • Import Spec
    • Value Spec
    • Type Spec
  • BadDecl 错误申明
  • GenDecl 一般申明 (和 Spec 相关, 比如 import “a”,var a,type a)
  • FuncDecl 函数申明

Files and Packages

  • File 代表一个源文件节点, 包含了顶级元素.
  • Package 代表一个包, 包含了很多文件.

全类型匹配

那么我们需要仔细判断上面的总总结构,来适配我们的特征:

  1. package go_code_analysis
  2. import (
  3. "fmt"
  4. "go/ast"
  5. "go/token"
  6. "log"
  7. )
  8. var GFset *token.FileSet
  9. var GFixedFunc map[string]Fixed //key的格式为Package.Func
  10. func stmtCase(stmt ast.Stmt, todo func(call *ast.CallExpr) bool) bool {
  11. switch t := stmt.(type) {
  12. case *ast.ExprStmt:
  13. log.Printf("表达式语句%+v at line:%v", t, GFset.Position(t.Pos()))
  14. if call, ok := t.X.(*ast.CallExpr); ok {
  15. return todo(call)
  16. }
  17. case *ast.ReturnStmt:
  18. for i, p := range t.Results {
  19. log.Printf("return语句%d:%v at line:%v", i, p, GFset.Position(p.Pos()))
  20. if call, ok := p.(*ast.CallExpr); ok {
  21. return todo(call)
  22. }
  23. }
  24. case *ast.AssignStmt:
  25. //函数体里的构造类型 999
  26. for _, p := range t.Rhs {
  27. switch t := p.(type) {
  28. case *ast.CompositeLit:
  29. for i, p := range t.Elts {
  30. switch t := p.(type) {
  31. case *ast.KeyValueExpr:
  32. log.Printf("构造赋值语句%d:%+v at line:%v", i, t.Value, GFset.Position(p.Pos()))
  33. if call, ok := t.Value.(*ast.CallExpr); ok {
  34. return todo(call)
  35. }
  36. }
  37. }
  38. }
  39. }
  40. default:
  41. log.Printf("不匹配的类型:%T", stmt)
  42. }
  43. return false
  44. }
  45. //调用函数的N种情况
  46. //对函数调用使用todo适配,并返回是否适配成功
  47. func AllCallCase(n ast.Node, todo func(call *ast.CallExpr) bool) (find bool) {
  48. //函数体里的直接调用 000
  49. if fn, ok := n.(*ast.FuncDecl); ok {
  50. for i, p := range fn.Body.List {
  51. log.Printf("函数体表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  52. find = find || stmtCase(p, todo)
  53. }
  54. log.Printf("func:%+v done", fn.Name.Name)
  55. }
  56. //if语句里
  57. if ifstmt, ok := n.(*ast.IfStmt); ok {
  58. log.Printf("if语句开始:%T %+v", ifstmt, GFset.Position(ifstmt.If))
  59. //if的赋值表达式 111
  60. if a, ok := ifstmt.Init.(*ast.AssignStmt); ok {
  61. for i, p := range a.Rhs {
  62. log.Printf("if语句赋值%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  63. switch call := p.(type) {
  64. case *ast.CallExpr:
  65. c := todo(call)
  66. find = find || c
  67. }
  68. }
  69. }
  70. //if的花括号里面 222
  71. for i, p := range ifstmt.Body.List {
  72. log.Printf("if语句内部表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  73. c := stmtCase(p, todo)
  74. find = find || c
  75. }
  76. //if的else里面 333
  77. if b, ok := ifstmt.Else.(*ast.BlockStmt); ok {
  78. for i, p := range b.List {
  79. log.Printf("if语句else表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  80. c := stmtCase(p, todo)
  81. find = find || c
  82. }
  83. }
  84. log.Printf("if语句结束:%+v done", GFset.Position(ifstmt.End()))
  85. }
  86. //赋值语句 444
  87. if assign, ok := n.(*ast.AssignStmt); ok {
  88. log.Printf("赋值语句开始:%T %s", assign, GFset.Position(assign.Pos()))
  89. for i, p := range assign.Rhs {
  90. log.Printf("赋值表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  91. switch t := p.(type) {
  92. case *ast.CallExpr:
  93. c := todo(t)
  94. find = find || c
  95. case *ast.CompositeLit:
  96. for i, p := range t.Elts {
  97. switch t := p.(type) {
  98. case *ast.KeyValueExpr:
  99. log.Printf("构造赋值%d:%+v at line:%v", i, t.Value, GFset.Position(p.Pos()))
  100. if call, ok := t.Value.(*ast.CallExpr); ok {
  101. c := todo(call)
  102. find = find || c
  103. }
  104. }
  105. }
  106. }
  107. }
  108. }
  109. if gostmt, ok := n.(*ast.GoStmt); ok {
  110. log.Printf("go语句开始:%T %s", gostmt.Call.Fun, GFset.Position(gostmt.Go))
  111. //go后面直接调用 555
  112. c := todo(gostmt.Call)
  113. find = find || c
  114. //go func里面的调用 666
  115. if g, ok := gostmt.Call.Fun.(*ast.FuncLit); ok {
  116. for i, p := range g.Body.List {
  117. log.Printf("go语句表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  118. c := stmtCase(p, todo)
  119. find = find || c
  120. }
  121. }
  122. log.Printf("go语句结束:%+v done", GFset.Position(gostmt.Go))
  123. }
  124. if deferstmt, ok := n.(*ast.DeferStmt); ok {
  125. log.Printf("defer语句开始:%T %s", deferstmt.Call.Fun, GFset.Position(deferstmt.Defer))
  126. //defer后面直接调用 777
  127. c := todo(deferstmt.Call)
  128. find = find || c
  129. //defer func里面的调用 888
  130. if g, ok := deferstmt.Call.Fun.(*ast.FuncLit); ok {
  131. for i, p := range g.Body.List {
  132. log.Printf("defer语句内部表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  133. c := stmtCase(p, todo)
  134. find = find || c
  135. }
  136. }
  137. log.Printf("defer语句结束:%+v done", GFset.Position(deferstmt.Defer))
  138. }
  139. if fostmt, ok := n.(*ast.ForStmt); ok {
  140. //for语句对应aaa和bbb
  141. log.Printf("for语句开始:%T %s", fostmt.Body, GFset.Position(fostmt.Pos()))
  142. for i, p := range fostmt.Body.List {
  143. log.Printf("for语句函数体表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  144. c := stmtCase(p, todo)
  145. find = find || c
  146. }
  147. }
  148. if rangestmt, ok := n.(*ast.RangeStmt); ok {
  149. //range语句对应ccc
  150. log.Printf("range语句开始:%T %s", rangestmt.Body, GFset.Position(rangestmt.Pos()))
  151. for i, p := range rangestmt.Body.List {
  152. log.Printf("range语句函数体表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  153. c := stmtCase(p, todo)
  154. find = find || c
  155. }
  156. }
  157. return
  158. }
  159. type FindContext struct {
  160. File string
  161. Package string
  162. LocalFunc *ast.FuncDecl
  163. }
  164. func (f *FindContext) Visit(n ast.Node) ast.Visitor {
  165. if n == nil {
  166. return f
  167. }
  168. if fn, ok := n.(*ast.FuncDecl); ok {
  169. log.Printf("函数[%s.%s]开始 at line:%v", f.Package, fn.Name.Name, GFset.Position(fn.Pos()))
  170. f.LocalFunc = fn
  171. } else {
  172. log.Printf("类型%T at line:%v", n, GFset.Position(n.Pos()))
  173. }
  174. find := AllCallCase(n, f.FindCallFunc)
  175. if find {
  176. name := fmt.Sprintf("%s.%s", f.Package, f.LocalFunc.Name)
  177. GFixedFunc[name] = Fixed{FuncDesc: FuncDesc{f.File, f.Package, f.LocalFunc.Name.Name}}
  178. }
  179. return f
  180. }
  181. func (f *FindContext) FindCallFunc(call *ast.CallExpr) bool {
  182. if call == nil {
  183. return false
  184. }
  185. log.Printf("call func:%+v, %v", call.Fun, call.Args)
  186. if callFunc, ok := call.Fun.(*ast.SelectorExpr); ok {
  187. if fmt.Sprint(callFunc.X) == "context" && fmt.Sprint(callFunc.Sel) == "WithCancel" {
  188. if len(call.Args) > 0 {
  189. if argu, ok := call.Args[0].(*ast.Ident); ok {
  190. log.Printf("argu type:%T, %s", argu.Name, argu.String())
  191. if argu.Name == "nil" {
  192. location := fmt.Sprint(GFset.Position(argu.NamePos))
  193. log.Printf("找到关键函数:%s.%s at line:%v", callFunc.X, callFunc.Sel, location)
  194. return true
  195. }
  196. }
  197. }
  198. }
  199. }
  200. return false
  201. }

AllCallCase方法中我们穷举了所有的调用函数的情况(ast.CallExpr),分别对应了 000 到 ccc 这 13 种情况。stmtCase方法分析了语句的各种可能,尽量找全所有。 FindContext.FindCallFunc方法首先看调用函数是不是选择结构,类似于 a.b 的结构;然后对比了调用函数的 a.b 是不是我们关心的context.WithCancel;最后看第一个实参的名称是不是nil

最终找到了所有特征点:

  1. 2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:9:21
  2. 2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:11:34
  3. 2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:12:22
  4. 2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:14:22
  5. 2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:11:34
  6. 2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:17:28
  7. 2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:19:24
  8. 2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:22:22
  9. 2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:25:27
  10. 2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:28:22
  11. 2019/01/16 20:19:52 找到关键函数:context.WithCancel at line:./example/test2.go:45:22

故事的结尾,我们使用 FindContext 提供的 walk 方法递归了 AST 树,找到了所有符合我们特征的函数,当然例子里就test一个函数。所有代码都在https://github.com/baixiaoustc/go_code_analysis中能找到。
https://baixiaoustc.github.io/2019/01/14/2019-01-14-golang-code-inspector-1-all-case/