簡介
今天我們介紹一個比較好玩的庫[govaluate](https://github.com/Knetic/govaluate)。govaluate與 JavaScript 中的eval功能類似,用于計算任意表達式的值。此類功能函數(shù)在 JavaScript/Python 等動態(tài)語言中比較常見。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計算表達式只需要兩步:
- 調(diào)用
NewEvaluableExpression()將表達式轉(zhuǎn)為一個表達式對象; - 調(diào)用表達式對象的
Evaluate方法,傳入?yún)?shù),返回表達式的值。
上面演示了一個很簡單的例子,我們使用govaluate計算10 > 0的值,該表達式不需要參數(shù),故傳給Evaluate()方法nil值。當(dāng)然,這個例子并不實用,顯然我們直接在代碼中計算10 > 0更簡單。但問題是,有些時候我們并不知道需要計算的表達式的所有信息,甚至我們都不知道表達式的結(jié)構(gòu)。這時govaluate的作用就體現(xiàn)出來了。
參數(shù)
govaluate支持在表達式中使用參數(shù),調(diào)用表達式對象的Evaluate()方法時通過map[string]interface{}類型將參數(shù)傳入計算。其中map的鍵為參數(shù)名,值為參數(shù)值。例如:
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的結(jié)果,在傳入?yún)?shù)中將foo設(shè)置為 -1,最終輸出false。
第二個表達式中,我們想要計算(requests_made * requests_succeeded / 100) >= 90的值,在參數(shù)中設(shè)置requests_made為 100,requests_succeeded為 80,結(jié)果為true。
上面兩個表達式都返回bool結(jié)果,第三個表達式返回一個浮點數(shù)。(mem_used / total_mem) * 100根據(jù)傳入的總內(nèi)存total_mem和當(dāng)前使用內(nèi)存mem_used,返回內(nèi)存占用百分比,結(jié)果為 50。
命名
使用govaluate與直接編寫 Go 代碼不同,在 Go 代碼中標(biāo)識符中不能出現(xiàn)-、+、$等符號。govaluate可以通過轉(zhuǎn)義使用這些符號。有兩種轉(zhuǎn)義方式:
- 將名稱用
[和]包裹起來,例如[response-time]; - 使用
\將緊接著下一個的字符轉(zhuǎn)義。
例如:
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)
}
注意一點,因為在字符串中\本身就是需要轉(zhuǎn)義的,所以在第二個表達式中要使用\\?;蛘呖梢允褂?/p>
`response\-time` < 100
一次“編譯”多次運行
使用帶參數(shù)的表達式,我們可以實現(xiàn)一個表達式的一次“編譯”,多次運行。只需要使用編譯返回的表達式對象即可,可多次調(diào)用其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)
}
第一次運行,傳入?yún)?shù)a = 1, b = 2得到結(jié)果 3;第二次運行,傳入?yún)?shù)a = 10, b = 20得到結(jié)果 30。
函數(shù)
如果僅僅能進行常規(guī)的算數(shù)和邏輯運算,govaluate的功能會大打折扣。govaluate提供了自定義函數(shù)的功能。所有自定義函數(shù)需要先定義好,存入一個map[string]govaluate.ExpressionFunction變量中,然后調(diào)用govaluate.NewEvaluableExpressionWithFunctions()生成表達式,此表達式中就可以使用這些函數(shù)了。自定義函數(shù)類型為func (args ...interface{}) (interface{}, error),如果函數(shù)返回錯誤,則這個表達式求值返回錯誤。
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)
}
上面例子中,我們定義一個函數(shù)strlen計算第一個參數(shù)的字符串長度。表達式strlen('teststring')調(diào)用strlen函數(shù)返回字符串teststring的長度。
函數(shù)可以接受任意數(shù)量的參數(shù),而且可以處理嵌套函數(shù)調(diào)用的問題。所以可以寫出類似下面這種復(fù)雜的表達式:
sqrt(x1 ** y1, x2 ** y2)
max(someValue, abs(anotherValue), 10 * lastValue)
訪問器
在 Go 語言中,訪問器(Accessors)就是通過.操作訪問結(jié)構(gòu)中的字段。如果傳入的參數(shù)中有結(jié)構(gòu)體類型,govaluate也支持使用.訪問其內(nèi)部字段或調(diào)用它們的方法:
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結(jié)構(gòu),并為它編寫了一個Fullname()方法。第一個表達式中,我們調(diào)用u.Fullname()返回全名,第二個表達式比較年齡是否大于 18。
需要注意的一點是,我們不能使用foo.SomeMap['key']的方式訪問map的值。由于訪問器涉及到很多反射,所以它一般比直接使用參數(shù)慢 4 倍左右。如果能使用參數(shù)的形式,盡量使用參數(shù)。在上面的例子中,我們可以直接調(diào)用u.Fullname(),將結(jié)果作為參數(shù)傳給表達式求值。涉及到復(fù)雜的計算可以通過自定義函數(shù)來解決。我們還可以實現(xiàn)govaluate.Parameter接口,對于表達式中使用的未知參數(shù),govaluate會自動調(diào)用其Get()方法獲?。?/p>
// src/github.com/Knetic/govaluate/parameters.go
type Parameters interface {
Get(name string) (interface{}, error)
}
例如,我們可以讓User實現(xiàn)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{}參數(shù)。另一個就是我們在這個例子中使用的Eval()方法,該方法接受一個Parameter接口。實際上,在Evaluate()實現(xiàn)內(nèi)部也是調(diào)用的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))
}
在表達式計算時,未知的參數(shù)都需要調(diào)用Parameter的Get()方法獲取。上面的例子中我們直接使用FullName就可以調(diào)用u.Get()方法返回全名。
支持的操作和類型
govaluate支持的操作和類型與 Go 語言有些不同。一方面govaluate中的類型和操作不如 Go 豐富,另一方面govaluate也對一些操作進行了擴展。
算數(shù)、比較和邏輯運算:
-
+``-``/``*``&``|``^``**``%``>>``<<:加減乘除,按位與,按位或,異或,乘方,取模,左移和右移; -
>``>=``<``<=``==``!=``=~``!~:=~為正則匹配,!~為正則不匹配; -
||``&&:邏輯或和邏輯與。
常量:
- 數(shù)字常量,
govaluate中將數(shù)字都作為 64 位浮點數(shù)處理; - 字符串常量,注意在
govaluate中,字符串用單引號'; - 日期時間常量,格式與字符串相同,
govaluate會嘗試自動解析字符串是否是日期,只支持 RFC3339、ISO8601等有限的格式; - 布爾常量:
true、false。
其他:
- 圓括號可以改變計算優(yōu)先級;
- 數(shù)組定義在
()中,每個元素之間用,分隔,可以支持任意的元素類型,如(1, 2, 'foo')。實際上在govaluate中數(shù)組是用[]interface{}來表示的; - 三目運算符:
? :。
在下面代碼中,govaluate會先將2014-01-02和2014-01-01 23:59:59轉(zhuǎn)為time.Time類型,然后再比較大?。?/p>
func main() {
expr, _ := govaluate.NewEvaluableExpression("'2014-01-02' > '2014-01-01 23:59:59'")
result, _ := expr.Evaluate(nil)
fmt.Println(result)
}
錯誤處理
在上面的例子中,我們刻意忽略了錯誤處理。實際上,govaluate在創(chuàng)建表達式對象和表達式求值這兩個操作中都可能產(chǎn)生錯誤。在生成表達式對象時,如果表達式有語法錯誤,則返回錯誤。表達式求值,如果傳入的參數(shù)不合法,或者某些參數(shù)缺失,或者訪問結(jié)構(gòu)體中不存在的字段都會報錯。
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,但是我們沒有傳入?yún)?shù)foo,執(zhí)行失?。?/p>
2020/04/01 22:33:07 evaluate error:No parameter 'foo' found.
其他錯誤可以自行驗證。
總結(jié)
govaluate雖然支持的操作和類型有限,也能實現(xiàn)比較有意思的功能。例如,可以寫一個 Web 服務(wù),由用戶自己編寫表達式,設(shè)置參數(shù),服務(wù)器算出結(jié)果。