Go AST 编程

677 阅读5分钟

一、什么是 AST?

AST,即抽象语法树。它是源代码语法结构的一 种抽象表示。 它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

image.png

image.png

并发、反射和 AST 三者是构建复杂系统(中间 件)的基石,真正的高级工具。

二、AST能干什么呢?

  • 精准的定位到声明语句、赋值语句、运算语句等等,实现对代码的分析、优化、变更等操作
  • 代码生成:Go 因为本身没有动态代理之类的工具,所以代 码生成大行其道,非常流行

开源例子

三、AST 编程入门

package ast

import (
   "fmt"
   "go/ast"
   "reflect"
)

type printVisitor struct {
}

func (t *printVisitor) Visit(node ast.Node) (w ast.Visitor) {
   if node == nil {
      fmt.Println(nil)
      return t
   }
   val := reflect.ValueOf(node)
   typ := reflect.TypeOf(node)
   for typ.Kind() == reflect.Ptr {
      typ = typ.Elem()
      val = val.Elem()
   }
   fmt.Printf("val: %+v, type: %s \n", val.Interface(), typ.Name())
   return t
}


func TestAst(t *testing.T) {
   fset := token.NewFileSet()
   f, err := parser.ParseFile(fset, "", "", parser.ParseComments)
   if err != nil {
      t.Fatal(err)
   }
   ast.Walk(&printVisitor{}, f)
}

image.png image.png

解析出来的 AST 树看上去可能不够优雅,AST 编程基本上都是一边打断点一边写,因为很难手动画出目标 AST 树。

3.1 基本步骤

  1. 使用 token.NewFileSet 来创建 FileSet (逻辑上的一种文件集合, 逻辑就意味着,其实它里面的文件可以不必真的放在磁盘上,而是手动添加进去)
  2. 使用 parser.ParseFile 开始解析源码,可以指定 mode
  3. 实现 ast.Visitor 的接口,作为遍历 AST 的处理 器,我们可以利用该实现达到读取、篡改 AST 树的目的
  4. 调用 ast.Walk 开始遍历 AST
func TestAst(t *testing.T) {
   fset := token.NewFileSet()
   f, err := parser.ParseFile(fset, "", "", parser.ParseComments)
   if err != nil {
      t.Fatal(err)
   }
   ast.Walk(&printVisitor{}, f)
}

image.png

3.2 Visitor 设计模式详解

Visitor 设计模式:这种设计模式常常用于遍历复杂的数据结构,典型的就是树形结构, 一些情况下可以用于遍历链表等结构。

image.png

  • 对于 Node 来说,就是接收 Visitor,然后具体的实现里面再调用 Visitor 的 Visit 方法,并且将自身传入下去。
  • 对于 Visitor 来说, 一般是要做类型判断,判 断该 Node 的实际类型,自己究竟能不能处理。

3.3 ast.Visitor 接口

Go 的 AST 包暴露了 Visitor 接口,用于 遍历整颗 AST 树, ast.Visitor 和普通的 Visitor 比起来,它 的返回值也是一个 Visitor,这个 Visitor 被用来进一步遍历剩下的节点。

image.png

3.4 ast.Node 接口

ast.Node 接口代表了 AST 的节点。任何一个 Go 语 句在 AST 里面都代表了一个节点,多个语句之间 可以组成一个更加复杂的节点。一个节点由三个关键子类组成

  • 表达式 Expr:例如各种内置类型、方法调用、 数组索引等
  • 语句 Stmt:如赋值语句、if 语句等
  • 声明 Decl:各种声

image.png

image.png

这三个关键类不需要记,写代码的时候再去看具体的 实现,因为总共有 69 种实现,Node 接口定义的方法其实不太常用,很多时候操 作 AST 都不关心 Pos,也就是位置。

3.4.1 GenDecl

GenDecl 代表通用声明,一般是:

  • 类型声明,以 type 开头的
  • 变量声明,以 var 开头的
  • 常量声明,以 const 开头的
  • import 声明,以 import 开头的

基本上,都要进一步判断 Tok 来判断究竟是什么声明。

image.png

image.png

3.4.2 FuncDecl

FuncDecl 是方法声明:

  • Doc 是文档。注意,不是所有的注释都是文档,要符合 Go 规范的文档
  • Recv 接收器
  • Name 方法名
  • Type 方法签名:里面包含泛型参数、参数类型、返回值

image.png

image.png

3.4.3 StructType

  • StructType 代表了一个结构体声明了Fields:字段。实际上,定义在该结构体上的方法是放在 ast.File 的 Decls 里

image.png

3.4.4 内置结构

  • 内置结构都有对应的 Node 实现。

image.png

image.png

3.5 开源实例

(1) ast.Walk 实现

image.png

image.png

例如方法声明,依次遍历它的文档、接收器、名字、参数类型、返回值类型和方法体。

3.6 代码演示

(1) 自定义注解

在 Java 里面有一种机制叫做注解,可以看做是特殊的接口。然而在Go 里面并没有,不过我们可以通过 AST 来设计一种全新的注解。这些注解将会被用于代码生成,可以看做是一种辅助的声明元数据的方式。

所谓注解(注释)

  • 单行注解形式:// @annotation_name annotation_value
  • 多行注解形式:/* @annotation_name annotation_value */

image.png

演示如何获得类型定义、方法声明、字段上的注解。

(2) 篡改源码 背景:

  • 一般为了 DEBUG,我们会考虑在方法调用之前先把请求输出到日志。
  • 一般的做法是手动写代码,但是我们可以考虑借助 AST 来完成。
package ast

import (
   "fmt"
)

func Hello(name string) string {
   return "hello, " + name
}

func Hello1(firstName, lastName string, age int) string {
   return fmt.Sprintf("hello, %s %s, %d", firstName, lastName, age)
}

关键步骤:

  • 加入 import
  • 加入 log 语句

image.png

3.7 面试要点

AST 在面试中大多数的情况下都是和编译原理一起面的。不过因为编译原理实在太难,大部分面试官根本不会面编译原理,所以只有在你面技术专家,或者编译相关岗位的时候才会面得比较多。

  • 什么是 AST(抽象语法树)?是源代码的一种树形结构的表示,我们可以通过修改 AST 来修改源码。
  • AST 编程有什么用?语法检查、格式检查、错误检查、自动补全、优化代码、代码生成,基本上就是只要你够强,用 AST 你就可以为所欲为,甚至设计新的语言。
  • Go 有没有注解?没有,但是我们可以用 AST 来自己设计。
  • 什么是 Visitor 设计模式?Visitor 模式主要用于遍历复杂结构体,要点在于接收一个节点作为输入,并且返回一个遍历子节点的 Visitor。