你好,我是小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 包打印出来的抽象语法树:

Go 打印 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 的威力相关推荐

  1. 『曹大带我学 Go 』系列文章汇总

    你好,我是小 X. 之前写了 11 篇跟着曹大学 Go 的文章,今天来汇总一下. 曹大的功力深厚,但能学到多少全看自己.第一期 Go 训练营也早就结束了,但学习还得继续.后面我也会继续发布这个系列,希 ...

  2. 曹大带我学 Go(8)—— 一个打点引发的事故

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 最近线上事故频发,搞得焦头烂额,但是能用上跟曹 ...

  3. 曹大带我学 Go(6)—— 技术之外

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 有学员私下和我说,这个课程挺打击他的自信心.我 ...

  4. 曹大带我学 Go(2)—— 迷惑的 goroutine 执行顺序

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 上一篇文章我们讲了 Go 调度的本质是一个生产 ...

  5. 曹大带我学 Go(12)—— 面向火焰图编程

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 现实中听过各种面向 XX 编程,什么面向过程编 ...

  6. 曹大带我学 Go(11)—— 从 map 的 extra 字段谈起

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 熟悉 map 结构体的读者应该知道,hmap ...

  7. 曹大带我学 Go(10)—— 如何给 Go 提性能优化的 pr

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 之前 qcrao 写了一篇<成为 Go ...

  8. 曹大带我学 Go(9)—— 开始积累自己的工具库

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 不知道你有没有这样的经验:看了很多计算机相关的 ...

  9. 曹大带我学 Go(7)—— 如何优雅地指定配置项

    你好,我是小X. 曹大最近开 Go 课程了,小X 正在和曹大学 Go. 这个系列会讲一些从课程中学到的让人醍醐灌顶的东西,拨云见日,带你重新认识 Go. 最近一个年久失修的库导致了线上事故,不得不去做 ...

最新文章

  1. 第2章 熟悉Eclipse开发工具---- System.out.println(sum=+(a+b));
  2. [CLPR] 用于加速训练神经网络的二阶方法
  3. css样式有行内式还有什么,在行内式CSS样式中,属性和值的书写规范与CSS样式规则不相同...
  4. Checksum 校验和
  5. 由一个bug引发的SQLite缓存一致性探索
  6. 「旅游信息管理系统」 · Java Swing + MySQL 开发
  7. idea中创建jsp项目详细步骤
  8. 【社工】社会工程学框架
  9. 【历史上的今天】10 月 11 日:域名 baidu.com 问世;三星 Galaxy Note 7 爆炸门告一段落;图灵奖数据库先驱诞生
  10. golang源码解析之chan
  11. 笔记本安装双系统教程
  12. dismiss和remove_rule out与dismiss的区别
  13. 机器学习在金融风控的经验总结!
  14. Qt项目的新首席维护人员
  15. 「自控原理」5.1 频率特性及其图示
  16. 淘宝跨境电商怎么做 淘宝跨境电商注意事项
  17. 【邢不行|量化小讲堂系列45-实战篇】关于股票市值:99%投资者不知道的坑,你知道吗?
  18. Python学习(类的属性、继承、覆盖等详解)
  19. 持之以恒,不仅仅是说说而已
  20. 读《海盗经济学》随笔一

热门文章

  1. WPF -- Xceed PropertyGrid应用详解
  2. php国际儿童绘画,北京FineArt儿童创意美术课6-8岁
  3. 美术集网校:面对不同角度人物这些素描线条有变化~
  4. safari快捷图标不见了_软件图标五花八门太难看?用它一键定制套装桌面
  5. Citrix Virtual Apps and Desktops Licesse激活方法
  6. 《空洞骑士》:我们为什么深爱这款玩起来看着像是自虐的游戏
  7. PVE踩坑实录2设置无线网卡
  8. ENSP中路由器配置详解
  9. python paramiko sftp_paramiko ssh sftp
  10. 真正靠谱的人:事不拖,话不多,人不作