引言
今天给大家介绍一个比较好玩的库,今天在看别人写的代码的时候发现在代码中用到了govaluate库,这个库可以让我们在golang代码中计算一个表达式的结果,这让我们在实现一些条件判断业务场景的时候非常便利。govaluate与 JavaScript 中的eval功能类似,用于计算任意表达式的值。此类功能函数在 JavaScript/Python 等动态语言中比较常见。govaluate让 Go 这个编译型语言也有了这个能力!
快速使用
安装
1. 2. $ go get github.com/Knetic/govaluate
使用
package main import ( "fmt" "github.com/Knetic/govaluate" "log" ) 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
的键为参数名,值为参数值。例如:
package main import ( "fmt" "github.com/Knetic/govaluate" ) func main() { expr1, _ := govaluate.NewEvaluableExpression("foo >0") param:= make(map[string]interface{}) param["foo"] = -1 r1, _ := expr1.Evaluate(param) fmt.Printf("r1:%+v\n",r1) expr2, _ := govaluate.NewEvaluableExpression("a * b") param1 := make(map[string]interface{}) param1["a"] = 2 param1["b"] =4 r2,_:= expr2.Evaluate(param1) fmt.Printf("r2:%+v",r2) expr3, _ := govaluate.NewEvaluableExpression("(a / b) * 100") param3 := make(map[string]interface{}) param3["a"] = 1024 param3["b"] = 512 r3,_:= expr3.Evaluate(param3) fmt.Printf("r3:%+v",r3) }
一次编译,多次运行
使用带参数的表达式,我们可以实现一个表达式的一次“编译”,多次运行。只需要使用编译返回的表达式对象即可,可多次调用其Evaluate()
方法:
package main import ( "fmt" "github.com/Knetic/govaluate" ) func main() { expr2, _ := govaluate.NewEvaluableExpression("a * b") param1 := make(map[string]interface{}) param1["a"] = 2 param1["b"] =4 r2,_:= expr2.Evaluate(param1) fmt.Printf("r2:%+v",r2) param2 := make(map[string]interface{}) param2["a"] = 5 param2["b"] =6 r23,_:= expr2.Evaluate(param2) fmt.Printf("r223:%+v",r23) }
上面的代码的运行结果,第一次运行 根据传递的参数得到的结果是8,第二次运行,由于我们更换了参数,但是还是复用了【a*b】的表达式,所以结果是30。
函数
如果仅仅能进行常规的算数和逻辑运算,govaluate的功能会大打折扣。govaluate提供了自定义函数的功能。所有自定义函数需要先定义好,存入一个map[string]govaluate.ExpressionFunction变量中,然后调用govaluate.NewEvaluableExpressionWithFunctions()生成表达式,此表达式中就可以使用这些函数了。自定义函数类型为func (args ...interface{}) (interface{}, error),如果函数返回错误,则这个表达式求值返回错误。
package main import ( "fmt" "github.com/Knetic/govaluate" ) func main() { funcs:= map[string]govaluate.ExpressionFunction{ "strlen":func(argus ...interface{})(interface{},error) { len:= len(argus[0].(string)) return len,nil }, } exprString:="strlen('testing')" expr, _ := govaluate.NewEvaluableExpressionWithFunctions(exprString, funcs) r, _ := expr.Evaluate(nil) fmt.Println(r) }
上面例子中,我们定义一个函数strlen计算第一个参数的字符串长度。表达式strlen('teststring')调用strlen函数返回字符串teststring的长度。
函数可以接受任意数量的参数,而且可以处理嵌套函数调用的问题。所以可以写出类似下面这种复杂的表达式:
sqrt(x1 ** y1, x2 ** y2) max(someValue, abs(anotherValue), 10 * lastValue)
访问器
在 Go 语言中,访问器(Accessors
)就是通过.
操作访问结构中的字段。如果传入的参数中有结构体类型,govaluate
也支持使用.
访问其内部字段或调用它们的方法:
package main import ( "fmt" "github.com/Knetic/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: "go", LastName: "jack", 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.go type Parameters interface { Get(name string) (interface{}, error) }
例如,我们可以让User实现Parameter接口:
package main import ( "errors" "fmt" "github.com/Knetic/govaluate" ) type User struct { FirstName string LastName string Age int } func (u User) FullName()string { return u.FirstName + u.LastName } 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: "go", LastName: "jack", Age: 18} expr1,_:=govaluate.NewEvaluableExpression("FullName") rr,_:=expr1.Eval(u) fmt.Println("user",rr) }
表达式对象实际上有两个方法,一个是我们前面用的Evaluate(),这个方法接受一个map[string]interface{}参数。另一个就是我们在这个例子中使用的Eval()方法,该方法接受一个Parameter接口。实际上,在Evaluate()实现内部也是调用的Eval()方法:
/ 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)) }
在表达式计算时,未知的参数都需要调用Parameter
的Get()
方法获取。上面的例子中我们直接使用FullName
就可以调用u.Get()
方法返回全名。
支持的操作和类型
govaluate支持的操作和类型与 Go 语言有些不同。一方面govaluate中的类型和操作不如 Go 丰富,另一方面govaluate也对一些操作进行了扩展。
算数、比较和逻辑运算:
+ - / * & | ^ ** % >> <<:加减乘除,按位与,按位或,异或,乘方,取模,左移和右移;
> >= < <= == != =~ !~:=~为正则匹配,!~为正则不匹配;
|| &&:逻辑或和逻辑与。
常量:
数字常量,govaluate中将数字都作为 64 位浮点数处理;
字符串常量,注意在govaluate中,字符串用单引号';
日期时间常量,格式与字符串相同,govaluate会尝试自动解析字符串是否是日期,只支持 RFC3339、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在创建表达式对象和表达式求值这两个操作中都可能产生错误。在生成表达式对象时,如果表达式有语法错误,则返回错误。表达式求值,如果传入的参数不合法,或者某些参数缺失,或者访问结构体中不存在的字段都会报错
总结
govaluate虽然支持的类型和操作优先,但是对于一些需要通过前端页面传递参数生成判断表达式的场景还是能非常好的实现,所以多掌握一些golang的库,可以让我们在业务实现的时候更加的灵活。