Go每日一库之27:govaluate

简介

今天我们介绍一个比较好玩的库govaluategovaluate与 JavaScript 中的eval功能类似,用于计算任意表达式的值。此类功能函数在 JavaScript/Python 等动态语言中比较常见。govaluate让 Go 这个编译型语言也有了这个能力!

快速使用

先安装:

bash 复制代码
$ go get github.com/Knetic/govaluate

后使用:

go 复制代码
package main

import (
  "fmt"
  "log"

  "github.com/Knetic/govaluate"
)

func main() {
  expr, err := govaluate.NewEvaluableExpression("10 > 0")
  if err != nil {
    log.Fatal("syntax error:", err)
  }

  result, err := expr.Evaluate(nil)
  if err != nil {
    log.Fatal("evaluate error:", err)
  }

  fmt.Println(result)
}

使用govaluate计算表达式只需要两步:

  • 调用NewEvaluableExpression()将表达式转为一个表达式对象
  • 调用表达式对象的Evaluate方法,传入参数,返回表达式的值。

上面演示了一个很简单的例子,我们使用govaluate计算10 > 0的值,该表达式不需要参数,故传给Evaluate()方法nil值。当然,这个例子并不实用,显然我们直接在代码中计算10 > 0更简单。但问题是,有些时候我们并不知道需要计算的表达式的所有信息,甚至我们都不知道表达式的结构。这时govaluate的作用就体现出来了。

参数

govaluate支持在表达式中使用参数,调用表达式对象的Evaluate()方法时通过map[string]interface{}类型将参数传入计算。其中map的键为参数名,值为参数值。例如:

go 复制代码
func main() {
  expr, _ := govaluate.NewEvaluableExpression("foo > 0")
  parameters := make(map[string]interface{})
  parameters["foo"] = -1
  result, _ := expr.Evaluate(parameters)
  fmt.Println(result)

  expr, _ = govaluate.NewEvaluableExpression("(requests_made * requests_succeeded / 100) >= 90")
  parameters = make(map[string]interface{})
  parameters["requests_made"] = 100
  parameters["requests_succeeded"] = 80
  result, _ = expr.Evaluate(parameters)
  fmt.Println(result)

  expr, _ = govaluate.NewEvaluableExpression("(mem_used / total_mem) * 100")
  parameters = make(map[string]interface{})
  parameters["total_mem"] = 1024
  parameters["mem_used"] = 512
  result, _ = expr.Evaluate(parameters)
  fmt.Println(result)
}

第一个表达式中,我们想要计算foo > 0的结果,在传入参数中将foo设置为 -1,最终输出false

第二个表达式中,我们想要计算(requests_made * requests_succeeded / 100) >= 90的值,在参数中设置requests_made为 100,requests_succeeded为 80,结果为true

上面两个表达式都返回bool结果,第三个表达式返回一个浮点数。(mem_used / total_mem) * 100根据传入的总内存total_mem和当前使用内存mem_used,返回内存占用百分比,结果为 50。

命名

使用govaluate与直接编写 Go 代码不同,在 Go 代码中标识符中不能出现-+$等符号。govaluate可以通过转义使用这些符号。有两种转义方式:

  • 将名称用[]包裹起来,例如[response-time]
  • 使用\将紧接着下一个的字符转义。

例如:

go 复制代码
func main() {
  expr, _ := govaluate.NewEvaluableExpression("[response-time] < 100")
  parameters := make(map[string]interface{})
  parameters["response-time"] = 80
  result, _ := expr.Evaluate(parameters)
  fmt.Println(result)

  expr, _ = govaluate.NewEvaluableExpression("response\\-time < 100")
  parameters = make(map[string]interface{})
  parameters["response-time"] = 80
  result, _ = expr.Evaluate(parameters)
  fmt.Println(result)
}

注意一点,因为在字符串中\本身就是需要转义的,所以在第二个表达式中要使用\\。或者可以使用

go 复制代码
`response\-time` < 100

一次"编译"多次运行

使用带参数的表达式,我们可以实现一个表达式的一次"编译",多次运行。只需要使用编译返回的表达式对象即可,可多次调用其Evaluate()方法:

go 复制代码
func main() {
  expr, _ := govaluate.NewEvaluableExpression("a + b")
  parameters := make(map[string]interface{})
  parameters["a"] = 1
  parameters["b"] = 2
  result, _ := expr.Evaluate(parameters)
  fmt.Println(result)

  parameters = make(map[string]interface{})
  parameters["a"] = 10
  parameters["b"] = 20
  result, _ = expr.Evaluate(parameters)
  fmt.Println(result)
}

第一次运行,传入参数a = 1, b = 2得到结果 3;第二次运行,传入参数a = 10, b = 20得到结果 30。

函数

如果仅仅能进行常规的算数和逻辑运算,govaluate的功能会大打折扣。govaluate提供了自定义函数的功能。所有自定义函数需要先定义好,存入一个map[string]govaluate.ExpressionFunction变量中,然后调用govaluate.NewEvaluableExpressionWithFunctions()生成表达式,此表达式中就可以使用这些函数了。自定义函数类型为func (args ...interface{}) (interface{}, error),如果函数返回错误,则这个表达式求值返回错误。

go 复制代码
func main() {
  functions := map[string]govaluate.ExpressionFunction{
    "strlen": func(args ...interface{}) (interface{}, error) {
      length := len(args[0].(string))
      return length, nil
    },
  }

  exprString := "strlen('teststring')"
  expr, _ := govaluate.NewEvaluableExpressionWithFunctions(exprString, functions)
  result, _ := expr.Evaluate(nil)
  fmt.Println(result)
}

上面例子中,我们定义一个函数strlen计算第一个参数的字符串长度。表达式strlen('teststring')调用strlen函数返回字符串teststring的长度。

函数可以接受任意数量的参数,而且可以处理嵌套函数调用的问题。所以可以写出类似下面这种复杂的表达式:

go 复制代码
sqrt(x1 ** y1, x2 ** y2)

max(someValue, abs(anotherValue), 10 * lastValue)

访问器

在 Go 语言中,访问器(Accessors)就是通过.操作访问结构中的字段。如果传入的参数中有结构体类型,govaluate也支持使用.访问其内部字段或调用它们的方法:

go 复制代码
type User struct {
  FirstName string
  LastName  string
  Age       int
}

func (u User) Fullname() string {
  return u.FirstName + " " + u.LastName
}

func main() {
  u := User{FirstName: "li", LastName: "dajun", Age: 18}
  parameters := make(map[string]interface{})
  parameters["u"] = u

  expr, _ := govaluate.NewEvaluableExpression("u.Fullname()")
  result, _ := expr.Evaluate(parameters)
  fmt.Println("user", result)

  expr, _ = govaluate.NewEvaluableExpression("u.Age > 18")
  result, _ = expr.Evaluate(parameters)
  fmt.Println("age > 18?", result)
}

在上面代码中,我们定义了一个User结构,并为它编写了一个Fullname()方法。第一个表达式中,我们调用u.Fullname()返回全名,第二个表达式比较年龄是否大于 18。

需要注意的一点是,我们不能使用foo.SomeMap['key']的方式访问map的值。由于访问器涉及到很多反射,所以它一般比直接使用参数慢 4 倍左右。如果能使用参数的形式,尽量使用参数。在上面的例子中,我们可以直接调用u.Fullname(),将结果作为参数传给表达式求值。涉及到复杂的计算可以通过自定义函数来解决。我们还可以实现govaluate.Parameter接口,对于表达式中使用的未知参数,govaluate会自动调用其Get()方法获取:

go 复制代码
// src/github.com/Knetic/govaluate/parameters.go
type Parameters interface {
  Get(name string) (interface{}, error)
}

例如,我们可以让User实现Parameter接口:

go 复制代码
type User struct {
  FirstName string
  LastName  string
  Age       int
}

func (u User) Get(name string) (interface{}, error) {
  if name == "FullName" {
    return u.FirstName + " " + u.LastName, nil
  }

  return nil, errors.New("unsupported field " + name)
}

func main() {
  u := User{FirstName: "li", LastName: "dajun", Age: 18}
  expr, _ := govaluate.NewEvaluableExpression("FullName")
  result, _ := expr.Eval(u)
  fmt.Println("user", result)
}

表达式对象实际上有两个方法,一个是我们前面用的Evaluate(),这个方法接受一个map[string]interface{}参数。另一个就是我们在这个例子中使用的Eval()方法,该方法接受一个Parameter接口。实际上,在Evaluate()实现内部也是调用的Eval()方法:

go 复制代码
// src/github.com/Knetic/govaluate/EvaluableExpression.go
func (this EvaluableExpression) Evaluate(parameters map[string]interface{}) (interface{}, error) {
  if parameters == nil {
    return this.Eval(nil)
  }
  return this.Eval(MapParameters(parameters))
}

在表达式计算时,未知的参数都需要调用ParameterGet()方法获取。上面的例子中我们直接使用FullName就可以调用u.Get()方法返回全名。

支持的操作和类型

govaluate支持的操作和类型与 Go 语言有些不同。一方面govaluate中的类型和操作不如 Go 丰富,另一方面govaluate也对一些操作进行了扩展。

算数、比较和逻辑运算:

  • + - / * & | ^ ** % >> <<:加减乘除,按位与,按位或,异或,乘方,取模,左移和右移;
  • > >= < <= == != =~ !~=~为正则匹配,!~为正则不匹配;
  • || &&:逻辑或和逻辑与。

常量:

  • 数字常量,govaluate中将数字都作为 64 位浮点数处理;
  • 字符串常量,注意在govaluate中,字符串用单引号'
  • 日期时间常量,格式与字符串相同,govaluate会尝试自动解析字符串是否是日期,只支持 RFC3339、ISO8601等有限的格式;
  • 布尔常量:truefalse

其他:

  • 圆括号可以改变计算优先级;
  • 数组定义在()中,每个元素之间用,分隔,可以支持任意的元素类型,如(1, 2, 'foo')。实际上在govaluate中数组是用[]interface{}来表示的;
  • 三目运算符:? :

在下面代码中,govaluate会先将2014-01-022014-01-01 23:59:59转为time.Time类型,然后再比较大小:

go 复制代码
func main() {
  expr, _ := govaluate.NewEvaluableExpression("'2014-01-02' > '2014-01-01 23:59:59'")
  result, _ := expr.Evaluate(nil)
  fmt.Println(result)
}

错误处理

在上面的例子中,我们刻意忽略了错误处理。实际上,govaluate在创建表达式对象和表达式求值这两个操作中都可能产生错误。在生成表达式对象时,如果表达式有语法错误,则返回错误。表达式求值,如果传入的参数不合法,或者某些参数缺失,或者访问结构体中不存在的字段都会报错。

go 复制代码
func main() {
  exprString := `>>>`
  expr, err := govaluate.NewEvaluableExpression(exprString)
  if err != nil {
    log.Fatal("syntax error:", err)
  }
  result, err := expr.Evaluate(nil)
  if err != nil {
    log.Fatal("evaluate error:", err)
  }
  fmt.Println(result)
}

我们可以依次修改表达式字符串,验证各种错误,首先是>>>

bash 复制代码
2020/04/01 22:31:59 syntax error:Invalid token: '>>>'

然后我们将其修改为foo > 0,但是我们没有传入参数foo,执行失败:

bash 复制代码
2020/04/01 22:33:07 evaluate error:No parameter 'foo' found.

其他错误可以自行验证。

总结

govaluate虽然支持的操作和类型有限,也能实现比较有意思的功能。例如,可以写一个 Web 服务,由用户自己编写表达式,设置参数,服务器算出结果。

大家如果发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄

参考

  1. govaluate GitHub:github.com/Knetic/gova...
  2. Go 每日一库 GitHub:github.com/go-quiz/go-...
相关推荐
却尘11 小时前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤12 小时前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt111 天前
AI DDD重构实践
go
Grassto3 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto4 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室5 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题5 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo
啊汉7 天前
古文观芷App搜索方案深度解析:打造极致性能的古文搜索引擎
go·软件随想
asaotomo8 天前
一款 AI 驱动的新一代安全运维代理 —— DeepSentry(深哨)
运维·人工智能·安全·ai·go
码界奇点8 天前
基于Gin与GORM的若依后台管理系统设计与实现
论文阅读·go·毕业设计·gin·源代码管理