Junedayday Blog

六月天天的个人博客

0%

【每周小结】2023-Week9

Go技巧 - 快速学习官方库ast

当我们谈及Go语言底层时,往往会聊GMP相关的并发原理,或者是以reflect为代表的反射处理,它们也是面试中的常客。

而我今天想推荐的一个底层库 - ast,全名抽象语法树(abstract syntax tree),不仅能让我们进一步掌握Go的基础语法,更是一个开发标准化提效工具的关键技能。

AST的基本概念

ast的官方概念理解起来比较复杂,有兴趣的可以参考这篇知乎,或者阅读更专业的资料。

对初学者来说,抓住上图中的两个关键因素:

  1. 树 - 了解树相关的深度遍历算法
  2. 节点 - 各语法特征,如变量、条件语句等

Go的AST节点类型

学习ast,从它底层的数据定义入手会让我们事半功倍。

ast语法的核心抽象是Node,定义如下:

1
2
3
4
type Node interface {
Pos() token.Pos // 起始定义的位置
End() token.Pos // 结束定义的位置
}

它的作用就是解析出对应语法的位置。整个Node相关的接口与实现比较复杂,我以网上的一个版本为例:

整个语法树很庞杂,每个接口与结构体都有自己的一些特点。为了方便大家加深这部分的理解,我们从一个具体的case入手

AST的示例代码

示例代码如下,即解析本身main.go文件,打印出import的库与类型定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)

func main() {
// 定义解析的具体文件
fSet := token.NewFileSet()
f, err := parser.ParseFile(fSet, "main.go", nil, parser.ParseComments)
if err != nil {
panic(err)
}

// 怎么解析 - 用visit对象去Walk(遍历)对应文件
visit := &MyVisitor{}
ast.Walk(visit, f)
}

type MyVisitor struct{}

// 具体处理的函数
func (vi *MyVisitor) Visit(node ast.Node) ast.Visitor {
switch node.(type) {
case *ast.ImportSpec:
// import库
fmt.Println(node.(*ast.ImportSpec).Path.Value)
case *ast.TypeSpec:
// 类型定义
fmt.Println(node.(*ast.TypeSpec).Name.Name)
}
return vi
}

其余语法的使用,大家可以参考前面图中的Node接口与实现的定义,对照着实现。

为了加深大家对ast这棵树的理解,我们再细化一下上面的例子。

AST的两种处理思路

示例中的import代码,即

1
2
3
4
5
6
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
)

它对应的ast中的结构体是

1
2
3
4
5
6
7
8
type GenDecl struct {
Doc *CommentGroup // associated documentation; or nil
TokPos token.Pos // position of Tok
Tok token.Token // IMPORT, CONST, TYPE, or VAR
Lparen token.Pos // position of '(', if any
Specs []Spec
Rparen token.Pos // position of ')', if any
}

而每个import选项,则是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 抽象
type Spec interface {
Node
specNode()
}

// 实现
type ImportSpec struct {
Doc *CommentGroup // associated documentation; or nil
Name *Ident // local package name (including "."); or nil
Path *BasicLit // import path
Comment *CommentGroup // line comments; or nil
EndPos token.Pos // end of spec (overrides Path.Pos if nonzero)
}

所以,我们可以有两种方式来访问到每个ImportSpec:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func (vi *MyVisitor) Visit(node ast.Node) ast.Visitor {
switch node.(type) {
// 方法1 - 直接处理下层的叶子节点
case *ast.ImportSpec:
// import库
fmt.Println(node.(*ast.ImportSpec).Path.Value)
// 方法2 - 先解析出上层节点,再处理下层节点
case *ast.GenDecl:
for _, v := range node.(*ast.GenDecl).Specs {
switch v.(type) {
case *ast.ImportSpec:
fmt.Println(v.(*ast.ImportSpec).Path.Value)
}
}
}
return vi
}

在用ast编写相关工具时,我建议优先按思路2去编写代码,它的优势在于两点:

  1. 扩大上下文信息 - 除了基本的ImportSpec,还可以使用父节点GenDecl中的共用信息
  2. 代码可读性高 - 先处理父节点,解析到具体的结构体,再进行各个子节点的处理,思路很自然

当整个Visit处理的内容比较多时,就需要进行一定的拆分,用递归来减少复杂度。

AST在日常工作中的使用示例

ast的特性看起来很酷,但很少会直接应用在项目的代码里。

不过,作为官方支持、可用来分析Go代码的库,它常用于制作二进制工具,在不同的场景使用:

  1. 进阶性的代码规范性检查 - 如检查某层代码的import情况,保证分层规范
  2. 自定义的代码生成 - 如根据注释自动生成定义,根据方法生成mock接口
  3. 编译前统一对库或方法的替换 - 在编译前,对某些特定的库或方法进行替换,修改原go文件

在优秀的框架中,ast往往与标准化相辅相成,形成正反馈:代码标准化的程度越高,ast就越能提升自动化、保障质量;ast应用得越广泛,代码的标准化程度自然就越高

编程思考 - 业务也是技术人员的核心能力

最近,为了项目的推进,我和大量的开发人员进行了沟通,发现部分新人对“业务”的认知有明显偏差。

技术有两个极致的方向:

  1. 底层理论性的研究
  2. 面向业务的trade-off

前者是为技术领域开辟新的领域,只有极少的研究员会参与这类工作;而面向业务的trade-off则是大多数开发者能接触到的终极目标,难点往往在于:

  1. 技术储备广 - 有多样的解决方案,各有利弊
  2. 业务认知深 - 洞察业务的核心价值
  3. 技术 ✖️ 业务 - 两者结合时的决策能力

技术是开发者的立身之本,而业务是公司的立身之本。 优秀的开发者并不在于能想出100个技术方案,而是能提出3个各有利弊的关键技术方案、并且能根据业务情况给出自己的意见。

工作生活 - 如果无法理解,也请包容他人

我有段时间心理洁癖非常严重:我会尝试各种方法、各种角度去理解一些人,但如果用尽方法、仍无法理解对方的所作所为,我就会对其非常反感(当然,这样的人是极少数的)。

理解是一个很理想的方式,能让我更深刻地了解对方,也更适合深度、长期的关系。但在现实生活中,我们很难投入那么多的时间与情绪去熟悉每个人,也因不可避免的个人认知偏差导致误会。

所以,在和大部分人相处时,包容是一种让自己更轻松的方式。

Github: https://github.com/Junedayday/code_reading

Blog: http://junes.tech/

Bilibili: https://space.bilibili.com/293775192

公众号: golangcoding

二维码