首页 > 其他分享 >Go每日一库之27:govaluate

Go每日一库之27:govaluate

时间:2023-09-28 09:03:50浏览次数:38  
标签:27 string parameters expr 一库 result Go govaluate 表达式

简介

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

快速使用

先安装:

$ go get github.com/Knetic/govaluate

后使用:

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的键为参数名,值为参数值。例如:

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]
  • 使用\将紧接着下一个的字符转义。

例如:

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.go
type 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.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类型,然后再比较大小:

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

标签:27,string,parameters,expr,一库,result,Go,govaluate,表达式
From: https://www.cnblogs.com/arena/p/17734805.html

相关文章

  • 20230927
    //approve,counter-offer,decide,end-user,lower,margin,profit,reduce,reduction,rough,supplier,asamatteroffact,bringdown,priceindication,refertoapprove-批准Toapprovemeanstoofficiallyagreetooracceptsomething.Itindicatesthe......
  • 【230927-7】若▲ABC的三个内角满足SinA:SinB:SinC=5:11:13,则▲ABC为(锐角/直角/钝角)
    ......
  • 【230927-5】已知F是双曲线的左焦点,过F作垂直于x轴的直线交该曲线的一条渐近线于M,若|F
    ......
  • 【230927-6】若双曲线C:(x/a)^2-(y/b)^2=1(a>0,b>0)的一条渐近线被圆(x-2)^2+y^2=4所
    ......
  • 20230927
    早上进行了英语课;中午回来,玩了一会儿后休息到下午;去购物,买了去亲戚家的月饼,裤子,还有崩崩崩联动的饮料套餐;晚上写完了数据结构作业,没看清楚题目要求,多浪费了时间,然后被PTA编辑器防止粘贴搞的很烦,一段调试用的代码不能随意的剪切放在不同地方,然后用python解决了这个......
  • 2023.09.27
     今天主要进行了四则运算FileReaderf=null;//创建文件读取对象BufferedReaderf1=null;//创建字符流读取对象Filefile=newFile("src//err_list.txt");try{f=newFileReader(file);f1=newBufferedReader(f);......
  • 9.27四则运算思路
    1.写一个控制操作数的函数,控制随机数范围,是否有括号,操作数,是否写入文件可以用数组储存数值用集合存运算符,存括号可以采用随机第一个左括号产生0到n-1之间,包左不包右,假设y,第二个的取值是第一个y+1到n若有括号n个数,n-1个运算符,数据结构,两个栈,一个存数,一个存运算符,然后运算得出结......
  • 大二打卡(9.27)
    今天做了什么:今天的课程只有英语,所以睡了会儿懒觉,结果差点睡过,九点将近一刻才起床,还好没迟到,今天的英语课我发现我的听力水平相对恢复了一点,但是对于单词的记忆模糊了,比如高三的时候讲过的关于custom和customs的区别,我记得多个s是跟海关或者规定啥有关的,结果加个s就是海关的意思,......
  • 2023.9.27
    今天早上起来看了看英语,做了一个九宫格和一个听写,老师说坚持半年质的飞跃,随后便打开idea继续完成我的四则运算,先把昨天写的bug改完,然后再写两个功能,总算是把自定义弄好了,类也写了一个,这样每天应该就能完成了,晚上看了看javaweb的内容,就休息了。......
  • 2023/9/27
    给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。有效字符串需满足:左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号都有一个对应的相同类型的左括号。 示例1:输入:s="()"输出:true示例 2:输入:s="()[]{}"输出:t......