Go expr 通用表達(dá)式引擎的使用
官方教程:https://expr-lang.org/docs/language-definition
官方Github:https://github.com/expr-lang/expr
文章所含代碼地址:https://github.com/ziyifast/ziyifast-code_instruction/tree/main/go-demo/go-expr
一、介紹
Expr表達(dá)式引擎是一個(gè)針對(duì)Go語言設(shè)計(jì)的動(dòng)態(tài)配置解決方案,它以簡(jiǎn)單的語法和強(qiáng)大的性能特性著稱。Expr表達(dá)式引擎的核心是安全、快速和直觀,很適合用于處理諸如訪問控制、數(shù)據(jù)過濾和資源管理等場(chǎng)景。在Go語言中應(yīng)用Expr,可以極大地提升應(yīng)用程序處理動(dòng)態(tài)規(guī)則的能力。不同于其他語言的解釋器或腳本引擎,Expr采用了靜態(tài)類型檢查,并且生成字節(jié)碼來執(zhí)行,因此它能同時(shí)保證性能和安全性。
二、安裝
//通過go get直接安裝即可 go get github.com/expr-lang/expr
三、使用
基礎(chǔ)使用
①運(yùn)行基本表達(dá)式
在下面例子中,表達(dá)式2 + 2被編譯成能運(yùn)行的字節(jié)碼,然后執(zhí)行這段字節(jié)碼并輸出結(jié)果。
同時(shí)下面的例子不包含變量,因此也不用傳入環(huán)境。
package main
import (
"fmt"
"github.com/expr-lang/expr"
)
func main() {
// 編譯一個(gè)基礎(chǔ)的加法表達(dá)式
program, err := expr.Compile(`2 + 2`)
if err != nil {
panic(err)
}
// 運(yùn)行編譯后的表達(dá)式,并沒有傳入環(huán)境,因?yàn)檫@里不需要使用任何變量
output, err := expr.Run(program, nil)
if err != nil {
panic(err)
}
// 打印結(jié)果
fmt.Println(output) // 輸出 4
}
②運(yùn)行變量表達(dá)式
下面我們創(chuàng)建一個(gè)包含變量的環(huán)境,編寫使用這些變量的表達(dá)式,編譯并運(yùn)行這個(gè)表達(dá)式。
在下面例子中,環(huán)境env包含了變量apple和banana。表達(dá)式apple + banana在編譯時(shí)會(huì)從環(huán)境中推斷apple和banana的類型,并在運(yùn)行時(shí)使用這些變量的值來評(píng)估表達(dá)式結(jié)果。
package main
import (
"fmt"
"github.com/expr-lang/expr"
)
func main() {
// 創(chuàng)建一個(gè)包含變量的環(huán)境
env := map[string]interface{}{
"apple": 5,
"banana": 10,
}
// 編譯一個(gè)使用環(huán)境中變量的表達(dá)式
program, err := expr.Compile(`apple + banana`, expr.Env(env))
if err != nil {
panic(err)
}
// 運(yùn)行表達(dá)式
output, err := expr.Run(program, env)
if err != nil {
panic(err)
}
// 打印結(jié)果
fmt.Println(output) // 輸出 15
}
語法介紹
下面主要是介紹 Expr 表達(dá)式引擎內(nèi)置函數(shù)的一部分。通過這些功能強(qiáng)大的函數(shù),可以更加靈活和高效地處理數(shù)據(jù)和邏輯。更詳細(xì)的函數(shù)列表和使用說明查閱官方函數(shù)文檔。
官方函數(shù)文檔:https://expr-lang.org/docs/language-definition
①字面量和變量
Expr表達(dá)式引擎能夠處理常見的數(shù)據(jù)類型字面量,包括數(shù)字、字符串和布爾值。字面量是直接在代碼中寫出的數(shù)據(jù)值,比如42、"hello"和true都是字面量
(1)字面量:數(shù)字、字符串、布爾值
// (1) 數(shù)字 42 // 表示整數(shù) 42 3.14 // 表示浮點(diǎn)數(shù) 3.14 // (2) 字符串 "hello, world" // 雙引號(hào)包裹的字符串,支持轉(zhuǎn)義字符 `hello, world` // 反引號(hào)包裹的字符串,保持字符串格式不變,不支持轉(zhuǎn)義 // (3)布爾值 true // 布爾真值 false // 布爾假值
(2)變量:Expr允許在環(huán)境中定義變量,然后在表達(dá)式中引用這些變量
// (1)表達(dá)式定義變量
env := map[string]interface{}{
"age": 25,
"name": "Alice",
}
// (2)表達(dá)式中引用變量
age > 18 // 檢查age是否大于18
name == "Alice" // 判斷name是否等于"Alice"
②運(yùn)算符
Expr表達(dá)式引擎支持多種運(yùn)算符,包含數(shù)學(xué)運(yùn)算符、邏輯運(yùn)算符、比較運(yùn)算符及集合運(yùn)算符等。
- 數(shù)學(xué)和邏輯運(yùn)算符
數(shù)學(xué)運(yùn)算符包括加(+)、減(-)、乘(*)、除(/)和取模(%)。邏輯運(yùn)算符包括邏輯與(&&)、邏輯或(||)和邏輯非(!)
2 + 2 // 計(jì)算結(jié)果為4 7 % 3 // 結(jié)果為1 !true // 結(jié)果為false age >= 18 && name == "Alice" // 檢查age是否不小于18且name是否等于"Alice"
- 比較運(yùn)算符
比較運(yùn)算符有相等(==)、不等(!=)、小于(<)、小于等于(<=)、大于(>)和大于等于(>=),用于比較兩個(gè)值
age == 25 // 檢查age是否等于25 age != 18 // 檢查age是否不等于18 age > 20 // 檢查age是否大于20
- 集合運(yùn)算符
Expr還提供了一些用于操作集合的運(yùn)算符,如in用于檢查元素是否在集合中,集合可以是數(shù)組、切片或字典
"user" in ["user", "admin"] // true,因?yàn)?user"在數(shù)組中
3 in {1: true, 2: false} // false,因?yàn)?不是字典的鍵
還有一些高級(jí)的集合操作函數(shù),比如all、any、one和none,這些函數(shù)需要結(jié)合匿名函數(shù)(lambda)使用:
all(tweets, {.Len <= 240}) // 檢查所有tweets的Len字段是否都不超過240
any(tweets, {.Len > 200}) // 檢查是否存在tweets的Len字段超過200
- 成員操作符
在Expr表達(dá)式語言中,成員操作符允許我們?cè)L問Go語言中struct的屬性。這個(gè)特性讓Expr可以直接操作復(fù)雜數(shù)據(jù)結(jié)構(gòu),非常地靈活實(shí)用。
// (1) 定義結(jié)構(gòu)體
type User struct {
Name string
Age int
}
// (2)訪問結(jié)構(gòu)體變量
env := map[string]interface{}{
"user": User{Name: "Alice", Age: 25},
}
code := `user.Name`
program, err := expr.Compile(code, expr.Env(env))
if err != nil {
panic(err)
}
output, err := expr.Run(program, env)
if err != nil {
panic(err)
}
fmt.Println(output) // 輸出: Alice
在操作結(jié)構(gòu)體變量時(shí),我們通常會(huì)需要判斷對(duì)應(yīng)字段值是否為空,這時(shí)就需要處理nil的情況:
在訪問屬性時(shí),可能會(huì)遇到對(duì)象是nil的情況。Expr提供了安全的屬性訪問,即使在結(jié)構(gòu)體或者嵌套屬性為nil的情況下,也不會(huì)拋出運(yùn)行時(shí)panic錯(cuò)誤。
方法一:使用?.操作符引用屬性,如果對(duì)象為nil則返回nil,而不會(huì)報(bào)錯(cuò)。
author.User?.Name // 等價(jià)于下面的表達(dá)式 author.User != nil ? author.User.Name : nil
方法二:??操作符,主要用于nil時(shí),返回默認(rèn)值
author.User?.Name ?? "Anonymous" // 等價(jià)于下面表達(dá)式 author.User != nil ? author.User.Name : "Anonymous"
③函數(shù)
Expr支持內(nèi)置函數(shù)和自定義函數(shù),使得表達(dá)式更加強(qiáng)大和靈活。
- 內(nèi)置函數(shù):內(nèi)置函數(shù)像len、all、none、any等可以直接在表達(dá)式中使用
- all:函數(shù) all 可以用來檢驗(yàn)集合中的元素是否全部滿足給定的條件。它接受兩個(gè)參數(shù),第一個(gè)參數(shù)是集合,第二個(gè)參數(shù)是條件表達(dá)式。
// 檢查所有 tweets 的 Content 長(zhǎng)度是否小于 240
code := `all(tweets, len(.Content) < 240)`
program, err := expr.Compile(code, expr.Env(env))
if err != nil {
panic(err)
}
- any:與 all 類似,any 函數(shù)用來檢測(cè)集合中是否有任一元素滿足條件。
// 檢查是否有任一 tweet 的 Content 長(zhǎng)度大于 240 code := `any(tweets, len(.Content) > 240)`
- none:用于檢查集合中沒有任何元素滿足條件。
// 確保沒有 tweets 是重復(fù)的 code := `none(tweets, .IsRepeated)`
// 內(nèi)置函數(shù)示例
program, err := expr.Compile(`all(users, {.Age >= 18})`, expr.Env(env))
if err != nil {
panic(err)
}
// 注意:這里env需要包含users變量,每個(gè)用戶都需要有Age屬性
output, err := expr.Run(program, env)
fmt.Print(output) // 如果env中所有用戶年齡都大于等于18,返回true
- 自定義函數(shù):通過在環(huán)境映射env中傳遞函數(shù)定義來創(chuàng)建自定義函數(shù)
在Expr中使用函數(shù)時(shí),我們可以讓代碼模塊化并在表達(dá)式中加入復(fù)雜邏輯。通過結(jié)合變量、運(yùn)算符和函數(shù)。但需要注意,在構(gòu)建Expr環(huán)境并運(yùn)行表達(dá)式時(shí),始終要確保類型安全。
// 自定義函數(shù)示例
env := map[string]interface{}{
"greet": func(name string) string {
return fmt.Sprintf("Hello, %s!", name)
},
}
program, err := expr.Compile(`greet("World")`, expr.Env(env))
if err != nil {
panic(err)
}
output, err := expr.Run(program, env)
fmt.Print(output) // 返回 Hello, World!
實(shí)際生產(chǎn)案例
比如我們現(xiàn)在有一個(gè)需求:電商平臺(tái)需要根據(jù)用戶屬性(會(huì)員等級(jí)、地域)和訂單信息(金額、商品類目),動(dòng)態(tài)配置促銷活動(dòng)的參與條件和折扣規(guī)則,無需修改代碼即可更新規(guī)則。
package main
import (
"fmt"
"log"
"time"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
)
// 用戶信息
type User struct {
ID int
Name string
Level int // 會(huì)員等級(jí)(1-普通, 2-黃金, 3-鉆石)
Region string // 用戶所在地區(qū)
JoinTime time.Time
}
// 訂單信息
type Order struct {
OrderID string
Amount float64 // 訂單金額
Category string // 商品類目(electronics, clothing, food)
CreatedTime time.Time
}
// 促銷規(guī)則配置
type PromotionRule struct {
Condition string // Expr表達(dá)式,判斷是否滿足條件
Discount float64 // 折扣比例(0.9表示9折)
}
// 初始化規(guī)則引擎環(huán)境
func createEnv(user User, order Order) map[string]interface{} {
return map[string]interface{}{
"User": user,
"Order": order,
"Now": time.Now(), // 內(nèi)置當(dāng)前時(shí)間函數(shù)
// 可添加其他輔助函數(shù),如字符串處理、數(shù)學(xué)計(jì)算等
}
}
// 編譯促銷規(guī)則條件
func compileRule(rule string) (*vm.Program, error) {
return expr.Compile(rule, expr.Env(createEnv(User{}, Order{})))
}
// 應(yīng)用促銷規(guī)則
func ApplyPromotion(user User, order Order, rule PromotionRule) (bool, float64, error) {
// 1. 編譯規(guī)則(生產(chǎn)環(huán)境需緩存編譯結(jié)果)
program, err := compileRule(rule.Condition)
if err != nil {
return false, 0, fmt.Errorf("規(guī)則編譯失敗: %v", err)
}
// 2. 創(chuàng)建執(zhí)行環(huán)境
env := createEnv(user, order)
// 3. 執(zhí)行規(guī)則判斷
output, err := expr.Run(program, env)
if err != nil {
return false, 0, fmt.Errorf("規(guī)則執(zhí)行失敗: %v", err)
}
// 4. 類型斷言判斷結(jié)果
conditionMet, ok := output.(bool)
if !ok {
return false, 0, fmt.Errorf("規(guī)則必須返回布爾值")
}
// 5. 返回是否滿足條件及折扣
return conditionMet, rule.Discount, nil
}
func main() {
// 模擬用戶和訂單數(shù)據(jù)
user := User{
ID: 1001,
Name: "Alice",
Level: 3,
Region: "CN",
JoinTime: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC),
}
order := Order{
OrderID: "20231020001",
Amount: 1500.00,
Category: "electronics",
CreatedTime: time.Now(),
}
// 從數(shù)據(jù)庫(kù)/配置中心讀取促銷規(guī)則(示例)
rules := []PromotionRule{
{
// 規(guī)則1:鉆石會(huì)員且訂單金額>1000,享85折
Condition: `User.Level >= 3 && Order.Amount > 1000 && Order.Category == "electronics"`,
Discount: 0.85,
},
{
// 規(guī)則2:注冊(cè)超過2年的用戶,任意訂單享9折
Condition: `Now.Sub(User.JoinTime).Hours() > 24*365*2`,
Discount: 0.9,
},
}
// 遍歷所有規(guī)則,應(yīng)用最優(yōu)折扣
bestDiscount := 1.0 // 默認(rèn)無折扣
for _, rule := range rules {
valid, discount, err := ApplyPromotion(user, order, rule)
if err != nil {
log.Printf("規(guī)則應(yīng)用錯(cuò)誤: %v", err)
continue
}
if valid && discount < bestDiscount {
bestDiscount = discount
}
}
// 計(jì)算最終價(jià)格
finalPrice := order.Amount * bestDiscount
fmt.Printf("原價(jià): ¥%.2f\n", order.Amount)
fmt.Printf("適用折扣: %.0f%%\n", (1-bestDiscount)*100)
fmt.Printf("最終價(jià)格: ¥%.2f\n", finalPrice)
}
運(yùn)行結(jié)果:

適用場(chǎng)景
總結(jié):規(guī)則變更頻繁且對(duì)吞吐要求不高 -> expr表達(dá)式,否則就直接上代碼
| 場(chǎng)景特征 | 推薦方案 | 理由 |
|---|---|---|
| 規(guī)則每天調(diào)整多次 | 表達(dá)式引擎 | 避免頻繁發(fā)版,提升業(yè)務(wù)敏捷性 |
| 規(guī)則復(fù)雜且嵌套業(yè)務(wù)對(duì)象 | 直接代碼 | 復(fù)雜邏輯更易維護(hù),編譯器輔助類型檢查 |
| 需非技術(shù)人員配置規(guī)則(產(chǎn)品/運(yùn)營(yíng)) | 表達(dá)式引擎 | 降低技術(shù)門檻,釋放開發(fā)資源 |
| 性能敏感(如:>10萬QPS) | 直接代碼 | 避免表達(dá)式解析開銷影響吞吐量 |
| 多租戶定制規(guī)則 | 表達(dá)式引擎 | 各租戶獨(dú)立配置,互不影響 |
還是以上面的電商場(chǎng)景為例,讓大家感受expr的好處以及使用場(chǎng)景:
場(chǎng)景:電商促銷規(guī)則判斷
需求:根據(jù)用戶等級(jí)、訂單金額、商品類目動(dòng)態(tài)調(diào)整折扣。
方案一:表達(dá)式引擎(expr)
// 規(guī)則配置(存儲(chǔ)于數(shù)據(jù)庫(kù))
rules := []PromotionRule{
{
Condition: `User.Level >= 3 && Order.Amount > 1000 && Order.Category == "electronics"`,
Discount: 0.85,
},
}
// 動(dòng)態(tài)執(zhí)行
valid, _ := ApplyPromotion(user, order, rule)
優(yōu)勢(shì):
- 運(yùn)營(yíng)人員可通過管理后臺(tái)隨時(shí)新增/修改規(guī)則,無需等待版本發(fā)布。
- 支持A/B測(cè)試:為不同用戶組配置不同規(guī)則。
劣勢(shì):
- 需額外開發(fā)規(guī)則管理界面和測(cè)試工具。
方案二:直接代碼判斷
func IsPromotionValid(user User, order Order) bool {
return user.Level >= 3 &&
order.Amount > 1000 &&
order.Category == "electronics"
}
優(yōu)勢(shì):
- 性能極高,適合每秒數(shù)十萬次調(diào)用的場(chǎng)景。
- 邏輯變更通過代碼評(píng)審,降低錯(cuò)誤風(fēng)險(xiǎn)。
劣勢(shì):
- 修改折扣條件需發(fā)版,無法快速響應(yīng)市場(chǎng)活動(dòng)。
到此這篇關(guān)于Go expr 通用表達(dá)式引擎的使用的文章就介紹到這了,更多相關(guān)Go expr 通用表達(dá)式引擎內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Go 語言入門學(xué)習(xí)之正則表達(dá)式
- Golang棧結(jié)構(gòu)和后綴表達(dá)式實(shí)現(xiàn)計(jì)算器示例
- 一文帶你全面掌握Go語言中的正則表達(dá)式
- Go語句與表達(dá)式案例手冊(cè)深度解析
- 在?Go?語言中使用?regexp?包處理正則表達(dá)式的操作
- Go語言實(shí)戰(zhàn)之詳細(xì)掌握正則表達(dá)式的應(yīng)用與技巧
- Golang中正則表達(dá)式語法及相關(guān)示例
- Go語言利用正則表達(dá)式處理多行文本
- Go中regexp包常見的正則表達(dá)式操作
- Go正則表達(dá)式匹配字符串,替換字符串方式
- Go語言結(jié)合正則表達(dá)式實(shí)現(xiàn)高效獲取數(shù)據(jù)
相關(guān)文章
一文帶你搞懂Golang結(jié)構(gòu)體內(nèi)存布局
結(jié)構(gòu)體在Go語言中是一個(gè)很重要的部分,在項(xiàng)目中會(huì)經(jīng)常用到。這篇文章主要帶大家看一下結(jié)構(gòu)體在內(nèi)存中是怎么分布的?通過對(duì)內(nèi)存布局的了解,可以幫助我們寫出更優(yōu)質(zhì)的代碼。感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助2022-10-10
go語言中的json與map相互轉(zhuǎn)換實(shí)現(xiàn)
本文主要介紹了go語言中的json與map相互轉(zhuǎn)換實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
Golang實(shí)踐之Error創(chuàng)建和處理詳解
在 C#、Java 等語言中常常使用 try...catch的方式來捕獲異常,但是在Golang 對(duì)于錯(cuò)誤處理有不同的方式,像網(wǎng)上也有很多對(duì) error 處理的最佳實(shí)踐的文章,其中很多其實(shí)就是對(duì) error 的統(tǒng)一封裝,使用規(guī)范進(jìn)行約束,本文主要是記錄自己對(duì)處理 Error 的一些認(rèn)識(shí)和學(xué)習(xí)2023-09-09
Go語言編程學(xué)習(xí)golang配置golint
這篇文章主要為大家介紹了Go語言編程學(xué)習(xí)golang配置golint的過程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2021-11-11
詳解go基于viper實(shí)現(xiàn)配置文件熱更新及其源碼分析
這篇文章主要介紹了詳解go基于viper實(shí)現(xiàn)配置文件熱更新及其源碼分析,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-06-06
golang 接口嵌套實(shí)現(xiàn)復(fù)用的操作
這篇文章主要介紹了golang 接口嵌套實(shí)現(xiàn)復(fù)用的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2021-04-04

