今天咱们介绍一个比较好玩的库govaluate
。govaluate
与 JavaScript 中的eval
功能相似,用于计算任意表达式的值。此类功能函数在 JavaScript/Python 等动态语言中比较常见。govaluate
让 Go 这个编译型语言也有了这个能力!git
先安装:github
$ go get github.com/Knetic/govaluate
后使用:数组
package mainimport ( "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
的键为参数名,值为参数值。例如:函数
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
。this
上面两个表达式都返回bool
结果,第三个表达式返回一个浮点数。(mem_used / total_mem) * 100
根据传入的总内存total_mem
和当前使用内存mem_used
,返回内存占用百分比,结果为 50。lua
使用govaluate
与直接编写 Go 代码不一样,在 Go 代码中标识符中不能出现-
、+
、$
等符号。govaluate
能够经过转义使用这些符号。有两种转义方式:spa
[
和]
包裹起来,例如[response-time]
;\
将紧接着下一个的字符转义。例如:
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)}
注意一点,由于在字符串中\
自己就是须要转义的,因此在第二个表达式中要使用\\
。或者可使用
`response\-time` < 100
使用带参数的表达式,咱们能够实现一个表达式的一次“编译”,屡次运行。只须要使用编译返回的表达式对象便可,可屡次调用其Evaluate()
方法:
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)
,若是函数返回错误,则这个表达式求值返回错误。
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
的长度。
函数能够接受任意数量的参数,并且能够处理嵌套函数调用的问题。因此能够写出相似下面这种复杂的表达式:
sqrt(x1 ** y1, x2 ** y2)max(someValue, abs(anotherValue), 10 * lastValue)
在 Go 语言中,访问器(Accessors
)就是经过.
操做访问结构中的字段。若是传入的参数中有结构体类型,govaluate
也支持使用.
访问其内部字段或调用它们的方法:
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()
方法获取:
// src/github.com/Knetic/govaluate/parameters.gotype Parameters interface { Get(name string) (interface{}, error)}
例如,咱们可让User
实现Parameter
接口:
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()
方法:
// src/github.com/Knetic/govaluate/EvaluableExpression.gofunc (this EvaluableExpression) Evaluate(parameters map[string]interface{}) (interface{}, error) { if parameters == nil { return this.Eval(nil) } return this.Eval(MapParameters(parameters))}
在表达式计算时,未知的参数都须要调用Parameter
的Get()
方法获取。上面的例子中咱们直接使用FullName
就能够调用u.Get()
方法返回全名。
govaluate
支持的操做和类型与 Go 语言有些不一样。一方面govaluate
中的类型和操做不如 Go 丰富,另外一方面govaluate
也对一些操做进行了扩展。
算数、比较和逻辑运算:
+
-
/
*
&
|
^
**
%
>>
<<
:加减乘除,按位与,按位或,异或,乘方,取模,左移和右移;>
>=
<
<=
==
!=
=~
!~
:=~
为正则匹配,!~
为正则不匹配;||
&&
:逻辑或和逻辑与。常量:
govaluate
中将数字都做为 64 位浮点数处理;govaluate
中,字符串用单引号'
;govaluate
会尝试自动解析字符串是不是日期,只支持 RFC333九、ISO8601等有限的格式;true
、false
。其余:
()
中,每一个元素之间用,
分隔,能够支持任意的元素类型,如(1, 2, 'foo')
。实际上在govaluate
中数组是用[]interface{}
来表示的;? :
。在下面代码中,govaluate
会先将2014-01-02
和2014-01-01 23:59:59
转为time.Time
类型,而后再比较大小:
func main() { expr, _ := govaluate.NewEvaluableExpression("'2014-01-02' > '2014-01-01 23:59:59'") result, _ := expr.Evaluate(nil) fmt.Println(result)}
在上面的例子中,咱们刻意忽略了错误处理。实际上,govaluate
在建立表达式对象和表达式求值这两个操做中均可能产生错误。在生成表达式对象时,若是表达式有语法错误,则返回错误。表达式求值,若是传入的参数不合法,或者某些参数缺失,或者访问结构体中不存在的字段都会报错。
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)}
咱们能够依次修改表达式字符串,验证各类错误,首先是>>>
:
2020/04/01 22:31:59 syntax error:Invalid token: '>>>'
而后咱们将其修改成foo > 0
,可是咱们没有传入参数foo
,执行失败:
2020/04/01 22:33:07 evaluate error:No parameter 'foo' found.
其余错误能够自行验证。
govaluate
虽然支持的操做和类型有限,也能实现比较有意思的功能。例如,能够写一个 Web 服务,由用户本身编写表达式,设置参数,服务器算出结果。
你们若是发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄
我
-
欢迎关注个人微信公众号【GoUpUp】,共同窗习,一块儿进步~