表达式引擎,顾名思义就是将表达式解析成可执行的代码,比如:1+2
,1+2*3
,(1+2)*3
,"hello " + "world"
等,更复杂的表达式,比如:sum(1+2)+1+1/2.0
等。
在golang中为什么要表达式引擎?由于golang是静态语言,无法像其他语言一样动态执行表达式,比如:1+2
,"hello " + "world"
等,那么就需要将表达式解析成可执行的代码。
使用场景有哪些呢?
1+2*3
,(1+2)*3
等;1+2*3>5
,(1+2)*3>5
等;支持表达式符号
+ - * / % ^ & | << >> && || !
== != > >= < <= && || !
+
sum(1,2)
a=1,b=2,c=3
AST
实现实现表达式引擎的方式有很多,大部分还是使用 AST
实现,大体流程如图:
AST
AST
节点AST
节点进行遍历,执行对应 AST
节点操作(1)解析生成AST
解析生成 AST
在golang中,可以使用 go/ast
包实现,具体代码:
ast.Expr
节点ast.Expr
节点,如果节点中包含函数调用,则判断是否为内置函数,如果不是内置函数则报错func (e *Expr) parse(s string) error {
if s == "" {
return fmt.Errorf("parse error: empty string")
}
node, err := parser.ParseExpr(s)
if err != nil {
return err
}
e.root = node
v := &visitor{pool: e.pool}
ast.Walk(v, e.root)
return v.err
}
type visitor struct {
pool *Pool
err error
}
// Visit implements ast.Visitor Visit method
func (v *visitor) Visit(node ast.Node) ast.Visitor {
if n, ok := node.(*ast.CallExpr); ok {
if fnIdent, ok := n.Fun.(*ast.Ident); ok {
if _, ok := v.pool.builtinList[fnIdent.Name]; !ok {
v.err = fmt.Errorf("undefined function `%v`", fnIdent.Name)
}
} else {
v.err = fmt.Errorf("unsupported call expr")
}
}
return v
}
(2)递归遍历ast.Expr
ast.Expr
节点,判断节点的类型+ - * / % ^ & | << >> && || !
,则递归遍历左右节点ast.CallExpr
节点,则判断是否为内置函数,如果不是内置函数则报错func eval(e *Expr, getter Getter, node ast.Expr) (Value, error) {
switch n := node.(type) {
case *ast.Ident:
// support true/false
switch strings.ToLower(n.Name) {
case "true":
return Bool(true), nil
case "false":
return Bool(false), nil
}
if getter == nil {
return e.pool.onVarMissing(n.Name)
}
v, ok := getter.get(n.Name)
if !ok {
return e.pool.onVarMissing(n.Name)
}
return v, nil
case *ast.BasicLit:
pos := int64(n.ValuePos)
var v Value
if e.cacheValues != nil {
if v, ok := e.cacheValues[pos]; ok {
return v, nil
}
}
switch n.Kind {
case token.INT:
i, err := strconv.ParseInt(n.Value, 10, 64)
if err != nil {
return nil, err
}
v = Int(i)
case token.FLOAT:
f, err := strconv.ParseFloat(n.Value, 64)
if err != nil {
return nil, err
}
v = Float(f)
case token.CHAR, token.STRING:
s, err := strconv.Unquote(n.Value)
if err != nil {
return nil, err
}
v = Raw(s)
default:
return nil, fmt.Errorf("unsupported token: %s(%v)", n.Value, n.Kind)
}
if e.cacheValues != nil {
e.cacheValues[pos] = v
}
return v, nil
case *ast.ParenExpr:
return eval(e, getter, n.X)
case *ast.CallExpr:
args := make([]Value, 0, len(n.Args))
for _, arg := range n.Args {
val, err := eval(e, getter, arg)
if err != nil {
return nil, err
}
args = append(args, val)
}
if fnIdent, ok := n.Fun.(*ast.Ident); ok {
return e.pool.builtinCall(fnIdent.Name, args...)
}
return nil, fmt.Errorf("unexpected func type: %T", n.Fun)
case *ast.UnaryExpr:
switch n.Op {
case token.ADD:
return eval(e, getter, n.X)
case token.SUB:
return eval(e, getter, n.X)
case token.NOT:
x, err := eval(e, getter, n.X)
if err == nil {
x, err = x.Not()
}
return x, err
default:
return nil, fmt.Errorf("unsupported unary op: %v", n.Op)
}
case *ast.BinaryExpr:
x, err := eval(e, getter, n.X)
if err != nil {
return nil, err
}
y, err := eval(e, getter, n.Y)
if err != nil {
return nil, err
}
switch n.Op {
case token.ADD:
return x.Add(y)
case token.SUB:
return x.Sub(y)
case token.MUL:
return x.Mul(y)
case token.QUO:
return x.Quo(y)
case token.REM:
return x.Rem(y)
case token.XOR:
return x.Xor()
case token.OR:
return x.Or(y)
case token.AND:
return x.And(y)
case token.SHL:
return x.Shl(y)
case token.SHR:
return x.Shr(y)
case token.AND_NOT:
return x.AndNot(y)
case token.LAND:
return x.Land(y)
case token.LOR:
return x.Lor(y)
case token.EQL:
return x.Eq(y)
case token.NEQ:
return x.Ne(y)
case token.GTR:
return x.Gt(y)
case token.GEQ:
return x.Ge(y)
case token.LSS:
return x.Lt(y)
case token.LEQ:
return x.Le(y)
default:
return nil, fmt.Errorf("unexpected binary operator: %v", n.Op)
}
default:
return nil, fmt.Errorf("unexpected node type %v", n)
}
}
开源地址:https://github.com/linkxzhou/mylib.git
func main() {
const evalstr = `((2 << 3) + (10 % 3)) * (5 - (x * 2)) + (3.0 / y) * (2.0 + 1.0) && ((z + "World") == "Hello World")`
e, err := New(evalstr, nil)
if err != nil {
return
}
fmt.Printf("%v = %v", evalstr, e.Eval(map[string]interface{}{
"x": 3.0,
"y": 2.0,
"z": "Hello ",
}))
}
WithCacheValues(true)
)var builtin = map[string]BuiltinFn{
"sum": func(args ...interface{}) (interface{}, error) {
var sum int64
for _, v := range args {
if v1, ok := v.(int64); !ok {
return nil, fmt.Errorf("%v int64 invalid", v)
} else {
sum += v1
}
}
return sum, nil
},
}
func main() {
const evalstr = `1+2*3+sum(1,2,3)`
pool, _ := NewPool(WithBuiltinList(builtin))
e, err := New(evalstr, pool, WithCacheValues(true))
if err != nil {
return
}
fmt.Printf("%v = %v", evalstr, e.Eval())
}
经过几个版本迭代的优化,增加缓存,减少拷贝,在性能方面,已经可以满足大部分场景。
测试环境:
const BenchmarkExpr = `((2 << 3) + (10 % 3)) * (5 - (x * 2)) + (3.0 / y) * (2.0 + 1.0) && ((z + "World") == "Hello World")`
func BenchmarkExprCache(b *testing.B) {
pool, _ := NewPool(WithBuiltinList(builtin))
e, err := New(BenchmarkExpr, pool, WithCacheValues(true))
if err != nil {
b.Errorf("expr New error = %v", err)
return
}
for i := 0; i < b.N; i++ {
_, err := e.Eval(map[string]interface{}{
"x": 3.0,
"y": 2.0,
"z": "Hello ",
})
if err != nil {
b.Errorf("expr Eval error = %v", err)
return
}
}
}
测试结果:
% go test -bench . -benchtime 1s -cpu 4 -benchmem -cpuprofile cpu.pprof -memprofile mem.pprof
goos: darwin
goarch: arm64
pkg: github.com/linkxzhou/mylib/go/expr
BenchmarkExprCache-4 2193422 543.7 ns/op 192 B/op 19 allocs/op
PASS
ok github.com/linkxzhou/mylib/go/expr 5.394s
执行QPS:219w/s,平均每次执行耗时543.7纳秒,不过对于原生的性能,差别还是比较大,后续会将影响性能的递归改为循环,预计可以提升30%。