Go 每日一庫之 govaluate

簡介

今天咱們介紹一個比較好玩的庫govaluategovaluate與 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,結果爲truethis

上面兩個表達式都返回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))}

在表達式計算時,未知的參數都須要調用ParameterGet()方法獲取。上面的例子中咱們直接使用FullName就能夠調用u.Get()方法返回全名。

支持的操做和類型

govaluate支持的操做和類型與 Go 語言有些不一樣。一方面govaluate中的類型和操做不如 Go 豐富,另外一方面govaluate也對一些操做進行了擴展。

算數、比較和邏輯運算:

  • + - / * & | ^ ** % >> <<:加減乘除,按位與,按位或,異或,乘方,取模,左移和右移;
  • > >= < <= == != =~ !~=~爲正則匹配,!~爲正則不匹配;
  • || &&:邏輯或和邏輯與。

常量:

  • 數字常量,govaluate中將數字都做爲 64 位浮點數處理;
  • 字符串常量,注意在govaluate中,字符串用單引號'
  • 日期時間常量,格式與字符串相同,govaluate會嘗試自動解析字符串是不是日期,只支持 RFC333九、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😄

參考

  1. govaluate GitHub:https://github.com/Knetic/govaluate
  2. Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib


-

個人博客:https://darjun.github.io

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

相關文章
相關標籤/搜索