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

12019.03.29 09:40:46 字数 1,830 阅读 4,667

我们拿到一个 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. package token
  2. import "strconv"
  3. type Token int
  4. const (
  5. ILLEGAL Token = iota
  6. EOF
  7. COMMENT
  8. literal_beg
  9. IDENT
  10. INT
  11. FLOAT
  12. IMAG
  13. CHAR
  14. STRING
  15. literal_end
  16. ...略...
  17. )

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. fset := token.NewFileSet()
  12. f, err := parser.ParseFile(fset, "", src, 0)
  13. if err != nil {
  14. panic(err)
  15. }
  16. ast.Print(fset, f)
  17. }

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

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

image

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

  1. type File struct {
  2. Doc *CommentGroup
  3. Package token.Pos
  4. Name *Ident
  5. Decls []Decl
  6. Scope *Scope
  7. Imports []*ImportSpec
  8. Unresolved []*Ident
  9. Comments []*CommentGroup
  10. }

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

遍历 AST

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

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

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. func SayHello(words []string) bool {
  11. fmt.Println(joinStrings(words))
  12. return true
  13. }
  14. func joinStrings(words []string) string {
  15. return strings.Join(words, ", ")
  16. }

结果为:

  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. fset := token.NewFileSet()
  11. f, err := parser.ParseFile(fset, "", "package main; var a = 3", parser.ParseComments)
  12. if err != nil {
  13. panic(err)
  14. }
  15. var v Visitor
  16. ast.Walk(v, f)
  17. }

旨在递归地打印出所有的 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)
  8. if _, err := context.WithCancel(nil); err != nil {
  9. context.WithCancel(nil)
  10. } else {
  11. context.WithCancel(nil)
  12. }
  13. _, _ = context.WithCancel(nil)
  14. go context.WithCancel(nil)
  15. go func() {
  16. context.WithCancel(nil)
  17. }()
  18. defer context.WithCancel(nil)
  19. defer func() {
  20. context.WithCancel(nil)
  21. }()
  22. data := map[string]interface{}{
  23. "x2": context.WithValue(nil, "k", "v"),
  24. }
  25. fmt.Println(data)
  26. var keys []string = []string{"ccc"}
  27. for _, k := range keys {
  28. fmt.Println(k)
  29. context.WithCancel(nil)
  30. }
  31. }

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

AST 的结构定义

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

  1. type Node interface {
  2. Pos() token.Pos
  3. End() token.Pos
  4. }
  5. type Expr interface {
  6. Node
  7. exprNode()
  8. }
  9. type Stmt interface {
  10. Node
  11. stmtNode()
  12. }
  13. type Decl interface {
  14. Node
  15. declNode()
  16. }

语法有三个主体:表达式 (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
  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. for _, p := range t.Rhs {
  26. switch t := p.(type) {
  27. case *ast.CompositeLit:
  28. for i, p := range t.Elts {
  29. switch t := p.(type) {
  30. case *ast.KeyValueExpr:
  31. log.Printf("构造赋值语句%d:%+v at line:%v", i, t.Value, GFset.Position(p.Pos()))
  32. if call, ok := t.Value.(*ast.CallExpr); ok {
  33. return todo(call)
  34. }
  35. }
  36. }
  37. }
  38. }
  39. default:
  40. log.Printf("不匹配的类型:%T", stmt)
  41. }
  42. return false
  43. }
  44. func AllCallCase(n ast.Node, todo func(call *ast.CallExpr) bool) (find bool) {
  45. if fn, ok := n.(*ast.FuncDecl); ok {
  46. for i, p := range fn.Body.List {
  47. log.Printf("函数体表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  48. find = find || stmtCase(p, todo)
  49. }
  50. log.Printf("func:%+v done", fn.Name.Name)
  51. }
  52. if ifstmt, ok := n.(*ast.IfStmt); ok {
  53. log.Printf("if语句开始:%T %+v", ifstmt, GFset.Position(ifstmt.If))
  54. if a, ok := ifstmt.Init.(*ast.AssignStmt); ok {
  55. for i, p := range a.Rhs {
  56. log.Printf("if语句赋值%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  57. switch call := p.(type) {
  58. case *ast.CallExpr:
  59. c := todo(call)
  60. find = find || c
  61. }
  62. }
  63. }
  64. for i, p := range ifstmt.Body.List {
  65. log.Printf("if语句内部表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  66. c := stmtCase(p, todo)
  67. find = find || c
  68. }
  69. if b, ok := ifstmt.Else.(*ast.BlockStmt); ok {
  70. for i, p := range b.List {
  71. log.Printf("if语句else表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  72. c := stmtCase(p, todo)
  73. find = find || c
  74. }
  75. }
  76. log.Printf("if语句结束:%+v done", GFset.Position(ifstmt.End()))
  77. }
  78. if assign, ok := n.(*ast.AssignStmt); ok {
  79. log.Printf("赋值语句开始:%T %s", assign, GFset.Position(assign.Pos()))
  80. for i, p := range assign.Rhs {
  81. log.Printf("赋值表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  82. switch t := p.(type) {
  83. case *ast.CallExpr:
  84. c := todo(t)
  85. find = find || c
  86. case *ast.CompositeLit:
  87. for i, p := range t.Elts {
  88. switch t := p.(type) {
  89. case *ast.KeyValueExpr:
  90. log.Printf("构造赋值%d:%+v at line:%v", i, t.Value, GFset.Position(p.Pos()))
  91. if call, ok := t.Value.(*ast.CallExpr); ok {
  92. c := todo(call)
  93. find = find || c
  94. }
  95. }
  96. }
  97. }
  98. }
  99. }
  100. if gostmt, ok := n.(*ast.GoStmt); ok {
  101. log.Printf("go语句开始:%T %s", gostmt.Call.Fun, GFset.Position(gostmt.Go))
  102. c := todo(gostmt.Call)
  103. find = find || c
  104. if g, ok := gostmt.Call.Fun.(*ast.FuncLit); ok {
  105. for i, p := range g.Body.List {
  106. log.Printf("go语句表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  107. c := stmtCase(p, todo)
  108. find = find || c
  109. }
  110. }
  111. log.Printf("go语句结束:%+v done", GFset.Position(gostmt.Go))
  112. }
  113. if deferstmt, ok := n.(*ast.DeferStmt); ok {
  114. log.Printf("defer语句开始:%T %s", deferstmt.Call.Fun, GFset.Position(deferstmt.Defer))
  115. c := todo(deferstmt.Call)
  116. find = find || c
  117. if g, ok := deferstmt.Call.Fun.(*ast.FuncLit); ok {
  118. for i, p := range g.Body.List {
  119. log.Printf("defer语句内部表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  120. c := stmtCase(p, todo)
  121. find = find || c
  122. }
  123. }
  124. log.Printf("defer语句结束:%+v done", GFset.Position(deferstmt.Defer))
  125. }
  126. if fostmt, ok := n.(*ast.ForStmt); ok {
  127. log.Printf("for语句开始:%T %s", fostmt.Body, GFset.Position(fostmt.Pos()))
  128. for i, p := range fostmt.Body.List {
  129. log.Printf("for语句函数体表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  130. c := stmtCase(p, todo)
  131. find = find || c
  132. }
  133. }
  134. if rangestmt, ok := n.(*ast.RangeStmt); ok {
  135. log.Printf("range语句开始:%T %s", rangestmt.Body, GFset.Position(rangestmt.Pos()))
  136. for i, p := range rangestmt.Body.List {
  137. log.Printf("range语句函数体表达式%d:%T at line:%v", i, p, GFset.Position(p.Pos()))
  138. c := stmtCase(p, todo)
  139. find = find || c
  140. }
  141. }
  142. return
  143. }
  144. type FindContext struct {
  145. File string
  146. Package string
  147. LocalFunc *ast.FuncDecl
  148. }
  149. func (f *FindContext) Visit(n ast.Node) ast.Visitor {
  150. if n == nil {
  151. return f
  152. }
  153. if fn, ok := n.(*ast.FuncDecl); ok {
  154. log.Printf("函数[%s.%s]开始 at line:%v", f.Package, fn.Name.Name, GFset.Position(fn.Pos()))
  155. f.LocalFunc = fn
  156. } else {
  157. log.Printf("类型%T at line:%v", n, GFset.Position(n.Pos()))
  158. }
  159. find := AllCallCase(n, f.FindCallFunc)
  160. if find {
  161. name := fmt.Sprintf("%s.%s", f.Package, f.LocalFunc.Name)
  162. GFixedFunc[name] = Fixed{FuncDesc: FuncDesc{f.File, f.Package, f.LocalFunc.Name.Name}}
  163. }
  164. return f
  165. }
  166. func (f *FindContext) FindCallFunc(call *ast.CallExpr) bool {
  167. if call == nil {
  168. return false
  169. }
  170. log.Printf("call func:%+v, %v", call.Fun, call.Args)
  171. if callFunc, ok := call.Fun.(*ast.SelectorExpr); ok {
  172. if fmt.Sprint(callFunc.X) == "context" && fmt.Sprint(callFunc.Sel) == "WithCancel" {
  173. if len(call.Args) > 0 {
  174. if argu, ok := call.Args[0].(*ast.Ident); ok {
  175. log.Printf("argu type:%T, %s", argu.Name, argu.String())
  176. if argu.Name == "nil" {
  177. location := fmt.Sprint(GFset.Position(argu.NamePos))
  178. log.Printf("找到关键函数:%s.%s at line:%v", callFunc.X, callFunc.Sel, location)
  179. return true
  180. }
  181. }
  182. }
  183. }
  184. }
  185. return false
  186. }

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中能找到。

更多精彩内容,就在简书 APP

“小礼物走一走,来简书关注我”

还没有人赞赏,支持一下

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

总资产 10 共写了 2.7W 字获得 119 个赞共 62 个粉丝

被以下专题收入,发现更多相似内容

推荐阅读更多精彩内容

  • https://arslan.io/2017/09/14/the-ultimate-guide-to-writin...
    golang深入源代码系列之一:AST的遍历 - 简书 - 图4

  • 摘要:我以前构建过一个工具,以让生活更轻松。这个工具被称为: gomodifytags ,它会根据字段名称自动填充…
    golang深入源代码系列之一:AST的遍历 - 简书 - 图5

  • 文/张努力 韩愈在《师说》中写道:“古之学者必有师。师者,所以传道受业解惑也。人非生而知之者,孰能无惑?惑而不从师…
    golang深入源代码系列之一:AST的遍历 - 简书 - 图6

  • 该写点什么好呢?该记点什么好呢?又该想点什么做点什么呢?是啊,该留点什么才行! 或躺着或站着或走着或吃着亦或蹲着。…
    golang深入源代码系列之一:AST的遍历 - 简书 - 图7
    薏米菇凉
    阅读 108 评论 0 赞 0
    golang深入源代码系列之一:AST的遍历 - 简书 - 图8

  • 这周过去了,都忘记做过什么时候了,复盘一下。 每天 6.30 起床,7 点 30 出门上班,听音频,没学习 上班早会上来做了…
    golang深入源代码系列之一:AST的遍历 - 简书 - 图9
    書雅
    阅读 42 评论 0 赞 0
    https://www.jianshu.com/p/937d649039ec