曹大带我学 Go(4)—— 初识 ast 的威力
你好,我是小X。
曹大最近开 Go 课程了,小X 正在和曹大学 Go。
这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go。
抽象语法树是编译过程中的一个中间产物,一般简单了解一下就行了。但我们可以把 Go 语言的整个 parser 和 ast 包直接拿来用,在一些场景下有很大的威力。
什么是 ast 呢,我从维基百科上摘录了一段:
在计算机科学中,抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
核心就是说 ast 能以一种树的形式表示代码结构。有了树结构,就可以对它做遍历,能干很多事。
假定一个场景
假定一个场景:我们可以从司机平台的某个接口获取司机的各种特征,例如:年龄、订单数、收入、每天驾驶时长、驾龄、平均车速、被投诉次数……数据一般采用 json 来传递。
司机平台的运营小姐姐经常需要搞一些活动,例如选出:
订单数超过 10000,且驾龄超过 5 年的老司机
每天驾驶时小于 3 小时,且收入超过 500 的高效司机
年龄大于 40,且平均速度大于 70 的“狂野”司机
……
这些规则并不是固定的,经常在变化,但总归是各种司机特征的组合。
为了简化,我们选取 2 个特征,并用一个 Driver
结构体来表示:
type Driver struct {Orders intDrivingYears int
}
为了配合运营搞活动,我们需要根据运营给的规则来判断一个司机是否符合要求。
如果公司人多,可以安排一个 rd 专门伺候运营小姐姐,每次做活动都来手动修改代码,也不是不可以。并且其实挺简单,我们来写一个示例代码:
// 从第三方获取司机特征,json 表示
func getDriverRemote() []byte {return []byte(`{"orders":100000,"driving_years":18}`)
}// 判断是否为老司机
func isOldDriver(d *Driver) bool {if d.Orders > 10000 && d.DrivingYears > 5 {return true}return false
}func main() {bs := getDriverRemote()var d Driverjson.Unmarshal(bs, &d)fmt.Println(isOldDriver(&d))
}
直接来看 main
函数:getDriverRemote
模拟从第三方 RPC 获取一个司机的特征数据,用 json 表示。接着 json.Unmarshal
来反序列化 Driver
结构体。最后调用 isOldDriver
函数来判断此司机是否符合运营的规则。
isOldDriver
根据 Driver
结构体的 2 个字段使用 if 语句来判断此司机是否为老司机。
确实还挺简单。
但是每次更新规则还得经过一次完整的上线流程,也挺麻烦的。有没有更简单的办法呢?使得我们可以直接解析运营小组姐给我们的一个用字符串表示的规则,并直接返回一个 bool 型的值,表示是否满足条件。
有的!
接下来就是本文的核心内容,如何使用 ast 来完成同样的功能。
直观地理解如何用 ast 解析规则
使用 ast 包提供的一些函数,我们可以非常方便地将如下的规则字符串:
orders > 10000 && driving_years > 5
解析成一棵这样的二叉树:
其中,ast.BinaryExpr
代表一个二元表达式,它由 X 和 Y 以及符号 OP 三部分组成。最上面的一个 BinaryExpr
表示规则的左半部分和右半部分相与。
很明显,左半部分就是:orders > 10000
,而右半部分则是:driving_years > 5
。神奇的是,左半部分和右半部分恰好又都是一个二元表达式。
左半部分的 orders > 10000
其实也是最小的叶子节点,它可以算出来一个 bool 值。把它拆开来之后,又可以分成 X、Y、OP。X 是 orders
,OP 是 ">",Y 则是 "10000"。其中 X 表示一个标识符,是 ast.Ident 类型,Y 表示一个基本类型的字面量,例如 int 型、字符串型……是 ast.BasicLit 类型。
右半部分的 driving_years > 18
也可以照此拆分。
然后,从 json 中取出这个司机的 orders
字段的值为 100000,它比 10000 大,所以左半部分算出来为 true。同理,右半部分算出来也为 true。最后,再算最外层的 "&&",结果仍然为 true。
至此,直接根据规则字符串,我们就可以算出来结果。
如果写成程序的话,就是一个 dfs 的遍历过程。如果不是叶子结点,那就是二元表达式结点,那就一定有 X、Y、OP 部分。递归地遍历 X,如果 X 是叶子结点,那就结束递归,并计算出 X 的值……
这里再展示一个用 ast 包打印出来的抽象语法树:
上图中,1、2、3 表示最外层的二元表达式;4、5、6 则表示左边这个二元表达式。
结合这张图,再参考 ast 包的相关结构体 代码,就非常清晰了。例如 ast.BinaryExpr
的代码如下:
// A BinaryExpr node represents a binary expression.
BinaryExpr struct {X Expr // left operandOpPos token.Pos // position of OpOp token.Token // operatorY Expr // right operand
}
它有 X、Y、OP,甚至还解析出了 Op 的位置,用 OpPos 表示。
如果你还对实现感兴趣,那就继续看下面的原理分析部分,否则可以直接跳到结尾总结部分。
原理分析
还是用上面那个例子,我们直接写一个表达式:
orders > 10000 && driving_years > 5
接下来用 ast 来解析规则并判断真假。
func main() {m := map[string]int64{"orders": 100000, "driving_years": 18}rule := `orders > 10000 && driving_years > 5`fmt.Println(Eval(m, rule))
}
为了简单,我们直接用 map 来代替 json,道理是一样的,仅仅为了方便。
Eval
函数判断 rule
的真假:
// Eval : 计算 expr 的值
func Eval(m map[string]int64, expr string) (bool, error) {exprAst, err := parser.ParseExpr(expr)if err != nil {return false, err}// 打印 astfset := token.NewFileSet()ast.Print(fset, exprAst)return judge(exprAst, m), nil
}
先将表达式解析成 Expr
,接着调用 judge 函数计算结果:
// dfs
func judge(bop ast.Node, m map[string]int64) bool {// 叶子结点if isLeaf(bop) {// 断言成二元表达式expr := bop.(*ast.BinaryExpr)x := expr.X.(*ast.Ident) // 左边y := expr.Y.(*ast.BasicLit) // 右边// 如果是 ">" 符号if expr.Op == token.GTR {left := m[x.Name]right, _ := strconv.ParseInt(y.Value, 10, 64)return left > right}return false}// 不是叶子节点那么一定是 binary expression(我们目前只处理二元表达式)expr, ok := bop.(*ast.BinaryExpr)if !ok {println("this cannot be true")return false}// 递归地计算左节点和右节点的值switch expr.Op {case token.LAND:return judge(expr.X, m) && judge(expr.Y, m)case token.LOR:return judge(expr.X, m) || judge(expr.Y, m)}println("unsupported operator")return false
}
judge
使用 dfs 递归地计算表达式的值。
递归地终止条件是叶子节点:
// 判断是否是叶子节点
func isLeaf(bop ast.Node) bool {expr, ok := bop.(*ast.BinaryExpr)if !ok {return false}// 二元表达式的最小单位,左节点是标识符,右节点是值_, okL := expr.X.(*ast.Ident)_, okR := expr.Y.(*ast.BasicLit)if okL && okR {return true}return false
}
总结
今天这篇文章主要讲了如何用 ast 包和 parser 包解析一个二元表达式,并见识到了它的威力,利用它可以做成一个非常简单的规则引擎。
其实利用 ast 包还可以做更多有意思的事情。例如批量把 thrift 文件转化成 proto 文件、解析 sql 语句并做一些审计……
想要更深入的学习,可以看曹大这篇《golang 和 ast》[1],据曹大自己说,他可以在 30 分钟内完成一个项目的一个 api 的编写,非常霸气!不服喷他……
好了,这就是今天全部的内容了~ 我是小X,我们下期再见~
参考资料
[1]
《golang 和 ast》: https://xargin.com/ast/
欢迎关注曹大的 TechPaper 以及码农桃花源~
曹大带我学 Go(4)—— 初识 ast 的威力相关推荐
- 『曹大带我学 Go 』系列文章汇总
你好,我是小 X. 之前写了 11 篇跟着曹大学 Go 的文章,今天来汇总一下. 曹大的功力深厚,但能学到多少全看自己.第一期 Go 训练营也早就结束了,但学习还得继续.后面我也会继续发布这个系列,希 ...
- 曹大带我学 Go(8)—— 一个打点引发的事故
你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 最近线上事故频发,搞得焦头烂额,但是能用上跟曹 ...
- 曹大带我学 Go(6)—— 技术之外
你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 有学员私下和我说,这个课程挺打击他的自信心.我 ...
- 曹大带我学 Go(2)—— 迷惑的 goroutine 执行顺序
你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 上一篇文章我们讲了 Go 调度的本质是一个生产 ...
- 曹大带我学 Go(12)—— 面向火焰图编程
你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 现实中听过各种面向 XX 编程,什么面向过程编 ...
- 曹大带我学 Go(11)—— 从 map 的 extra 字段谈起
你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 熟悉 map 结构体的读者应该知道,hmap ...
- 曹大带我学 Go(10)—— 如何给 Go 提性能优化的 pr
你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 之前 qcrao 写了一篇<成为 Go ...
- 曹大带我学 Go(9)—— 开始积累自己的工具库
你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 不知道你有没有这样的经验:看了很多计算机相关的 ...
- 曹大带我学 Go(7)—— 如何优雅地指定配置项
你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 最近一个年久失修的库导致了线上事故,不得不去做 ...
最新文章
- 百度地图 app 点击事件无效、不触发 解决方案
- html 定义函数调用函数,请问HTML function函数怎么定义和调用?
- 漫画 | 苦逼项目是如何诞生的?
- java找不到符号类socket,编译报错+解决方法:错误: 找不到符号
- Spring文件上传
- 页面放在哪_思维制胜!PPT内容巨多的页面,这样排版更高大上
- 吓人!普京最新Deepfake视频来了,MIT现场伪造实时采访
- 周杰伦新歌《说好不哭》上线,程序员哭了......【华为云分享】
- Custom Sharepoint Lookup Field
- QT中文乱码的解决方法,一劳永股的解决方法,如下
- 年薪百万是社会认同,更是自身价值体现
- Python实现B样条代码
- 本特利电涡流传感器 330876-02-90-01-CN
- 中学计算机基础授课ppt,高中信息技术说课课件.ppt
- MySQL中Packet for query is too large (1327736 1048576)错误
- Python爬虫——Scrapy框架的基本使用
- 电脑远程vps时无法复制粘贴怎么办
- 项目管理知识体系指南 (四)
- 薪酬管理系统功能描述文档
- 企业数据治理体系一文读懂