Go expr 通用表达式引擎的使用

 更新时间:2025年05月25日 10:17:44   作者:NPE~  
本文主要介绍了Go expr 通用表达式引擎的使用,支持变量、运算符和函数,适用于动态规则管理,具有一定的参考价值,感兴趣的可以了解一下

官方教程: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表达式引擎是一个针对Go语言设计的动态配置解决方案,它以简单的语法和强大的性能特性著称。Expr表达式引擎的核心是安全、快速和直观,很适合用于处理诸如访问控制、数据过滤和资源管理等场景。在Go语言中应用Expr,可以极大地提升应用程序处理动态规则的能力。不同于其他语言的解释器或脚本引擎,Expr采用了静态类型检查,并且生成字节码来执行,因此它能同时保证性能和安全性。

二、安装

//通过go get直接安装即可
go get github.com/expr-lang/expr

三、使用

基础使用

①运行基本表达式

在下面例子中,表达式2 + 2被编译成能运行的字节码,然后执行这段字节码并输出结果。
同时下面的例子不包含变量,因此也不用传入环境。

package main

import (
        "fmt"
        "github.com/expr-lang/expr"
)

func main() {
        // 编译一个基础的加法表达式
        program, err := expr.Compile(`2 + 2`)
        if err != nil {
                panic(err)
        }

        // 运行编译后的表达式,并没有传入环境,因为这里不需要使用任何变量
        output, err := expr.Run(program, nil)
        if err != nil {
                panic(err)
        }

        // 打印结果
        fmt.Println(output)  // 输出 4
}

②运行变量表达式

下面我们创建一个包含变量的环境,编写使用这些变量的表达式,编译并运行这个表达式。
在下面例子中,环境env包含了变量apple和banana。表达式apple + banana在编译时会从环境中推断apple和banana的类型,并在运行时使用这些变量的值来评估表达式结果。

package main

import (
    "fmt"
    "github.com/expr-lang/expr"
)

func main() {
    // 创建一个包含变量的环境
    env := map[string]interface{}{
       "apple":  5,
       "banana": 10,
    }

    // 编译一个使用环境中变量的表达式
    program, err := expr.Compile(`apple + banana`, expr.Env(env))
    if err != nil {
       panic(err)
    }

    // 运行表达式
    output, err := expr.Run(program, env)
    if err != nil {
       panic(err)
    }

    // 打印结果
    fmt.Println(output) // 输出 15
}

语法介绍

下面主要是介绍 Expr 表达式引擎内置函数的一部分。通过这些功能强大的函数,可以更加灵活和高效地处理数据和逻辑。更详细的函数列表和使用说明查阅官方函数文档。

官方函数文档:https://expr-lang.org/docs/language-definition

①字面量和变量

Expr表达式引擎能够处理常见的数据类型字面量,包括数字、字符串和布尔值。字面量是直接在代码中写出的数据值,比如42、"hello"和true都是字面量

(1)字面量:数字、字符串、布尔值

// (1) 数字
42      // 表示整数 42
3.14    // 表示浮点数 3.14


// (2) 字符串
"hello, world" // 双引号包裹的字符串,支持转义字符
`hello, world` // 反引号包裹的字符串,保持字符串格式不变,不支持转义

// (3)布尔值
true   // 布尔真值
false  // 布尔假值

(2)变量:Expr允许在环境中定义变量,然后在表达式中引用这些变量

// (1)表达式定义变量
env := map[string]interface{}{
    "age": 25,
    "name": "Alice",
}

// (2)表达式中引用变量
age > 18  // 检查age是否大于18
name == "Alice"  // 判断name是否等于"Alice"

②运算符

Expr表达式引擎支持多种运算符,包含数学运算符、逻辑运算符、比较运算符及集合运算符等。

  • 数学和逻辑运算符

数学运算符包括加(+)、减(-)、乘(*)、除(/)和取模(%)。逻辑运算符包括逻辑与(&&)、逻辑或(||)和逻辑非(!)

2 + 2 // 计算结果为4
7 % 3 // 结果为1
!true // 结果为false
age >= 18 && name == "Alice" // 检查age是否不小于18且name是否等于"Alice"
  • 比较运算符

比较运算符有相等(==)、不等(!=)、小于(<)、小于等于(<=)、大于(>)和大于等于(>=),用于比较两个值

age == 25 // 检查age是否等于25
age != 18 // 检查age是否不等于18
age > 20  // 检查age是否大于20
  • 集合运算符

Expr还提供了一些用于操作集合的运算符,如in用于检查元素是否在集合中,集合可以是数组、切片或字典

"user" in ["user", "admin"]  // true,因为"user"在数组中
3 in {1: true, 2: false}     // false,因为3不是字典的键

还有一些高级的集合操作函数,比如all、any、one和none,这些函数需要结合匿名函数(lambda)使用:

all(tweets, {.Len <= 240})  // 检查所有tweets的Len字段是否都不超过240
any(tweets, {.Len > 200})   // 检查是否存在tweets的Len字段超过200
  • 成员操作符

在Expr表达式语言中,成员操作符允许我们访问Go语言中struct的属性。这个特性让Expr可以直接操作复杂数据结构,非常地灵活实用。

// (1) 定义结构体
type User struct {
    Name string
    Age  int
}

// (2)访问结构体变量
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

在操作结构体变量时,我们通常会需要判断对应字段值是否为空,这时就需要处理nil的情况:

在访问属性时,可能会遇到对象是nil的情况。Expr提供了安全的属性访问,即使在结构体或者嵌套属性为nil的情况下,也不会抛出运行时panic错误。

方法一:使用?.操作符引用属性,如果对象为nil则返回nil,而不会报错。

author.User?.Name

// 等价于下面的表达式
author.User != nil ? author.User.Name : nil

方法二:??操作符,主要用于nil时,返回默认值

author.User?.Name ?? "Anonymous"

// 等价于下面表达式
author.User != nil ? author.User.Name : "Anonymous"

③函数

Expr支持内置函数和自定义函数,使得表达式更加强大和灵活。

  • 内置函数:内置函数像len、all、none、any等可以直接在表达式中使用
  • all:函数 all 可以用来检验集合中的元素是否全部满足给定的条件。它接受两个参数,第一个参数是集合,第二个参数是条件表达式。
// 检查所有 tweets 的 Content 长度是否小于 240
code := `all(tweets, len(.Content) < 240)`
program, err := expr.Compile(code, expr.Env(env))
if err != nil {
    panic(err)
}
  • any:与 all 类似,any 函数用来检测集合中是否有任一元素满足条件。
// 检查是否有任一 tweet 的 Content 长度大于 240
code := `any(tweets, len(.Content) > 240)`
  • none:用于检查集合中没有任何元素满足条件。
// 确保没有 tweets 是重复的
code := `none(tweets, .IsRepeated)`
// 内置函数示例
program, err := expr.Compile(`all(users, {.Age >= 18})`, expr.Env(env))
if err != nil {
    panic(err)
}

// 注意:这里env需要包含users变量,每个用户都需要有Age属性
output, err := expr.Run(program, env)
fmt.Print(output) // 如果env中所有用户年龄都大于等于18,返回true
  • 自定义函数:通过在环境映射env中传递函数定义来创建自定义函数

在Expr中使用函数时,我们可以让代码模块化并在表达式中加入复杂逻辑。通过结合变量、运算符和函数。但需要注意,在构建Expr环境并运行表达式时,始终要确保类型安全。

// 自定义函数示例
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!

实际生产案例

比如我们现在有一个需求:电商平台需要根据用户属性(会员等级、地域)和订单信息(金额、商品类目),动态配置促销活动的参与条件和折扣规则,无需修改代码即可更新规则。

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    // 会员等级(1-普通, 2-黄金, 3-钻石)
    Region   string // 用户所在地区
    JoinTime time.Time
}

// 订单信息
type Order struct {
    OrderID     string
    Amount      float64 // 订单金额
    Category    string  // 商品类目(electronics, clothing, food)
    CreatedTime time.Time
}

// 促销规则配置
type PromotionRule struct {
    Condition string  // Expr表达式,判断是否满足条件
    Discount  float64 // 折扣比例(0.9表示9折)
}

// 初始化规则引擎环境
func createEnv(user User, order Order) map[string]interface{} {
    return map[string]interface{}{
       "User":  user,
       "Order": order,
       "Now":   time.Now(), // 内置当前时间函数
       // 可添加其他辅助函数,如字符串处理、数学计算等
    }
}

// 编译促销规则条件
func compileRule(rule string) (*vm.Program, error) {
    return expr.Compile(rule, expr.Env(createEnv(User{}, Order{})))
}

// 应用促销规则
func ApplyPromotion(user User, order Order, rule PromotionRule) (bool, float64, error) {
    // 1. 编译规则(生产环境需缓存编译结果)
    program, err := compileRule(rule.Condition)
    if err != nil {
       return false, 0, fmt.Errorf("规则编译失败: %v", err)
    }

    // 2. 创建执行环境
    env := createEnv(user, order)

    // 3. 执行规则判断
    output, err := expr.Run(program, env)
    if err != nil {
       return false, 0, fmt.Errorf("规则执行失败: %v", err)
    }

    // 4. 类型断言判断结果
    conditionMet, ok := output.(bool)
    if !ok {
       return false, 0, fmt.Errorf("规则必须返回布尔值")
    }

    // 5. 返回是否满足条件及折扣
    return conditionMet, rule.Discount, nil
}

func main() {
    // 模拟用户和订单数据
    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(),
    }

    // 从数据库/配置中心读取促销规则(示例)
    rules := []PromotionRule{
       {
          // 规则1:钻石会员且订单金额>1000,享85折
          Condition: `User.Level >= 3 && Order.Amount > 1000 && Order.Category == "electronics"`,
          Discount:  0.85,
       },
       {
          // 规则2:注册超过2年的用户,任意订单享9折
          Condition: `Now.Sub(User.JoinTime).Hours() > 24*365*2`,
          Discount:  0.9,
       },
    }

    // 遍历所有规则,应用最优折扣
    bestDiscount := 1.0 // 默认无折扣
    for _, rule := range rules {
       valid, discount, err := ApplyPromotion(user, order, rule)
       if err != nil {
          log.Printf("规则应用错误: %v", err)
          continue
       }
       if valid && discount < bestDiscount {
          bestDiscount = discount
       }
    }

    // 计算最终价格
    finalPrice := order.Amount * bestDiscount
    fmt.Printf("原价: ¥%.2f\n", order.Amount)
    fmt.Printf("适用折扣: %.0f%%\n", (1-bestDiscount)*100)
    fmt.Printf("最终价格: ¥%.2f\n", finalPrice)
}

运行结果:

在这里插入图片描述

适用场景

总结:规则变更频繁且对吞吐要求不高 -> expr表达式,否则就直接上代码

场景特征推荐方案理由
规则每天调整多次表达式引擎避免频繁发版,提升业务敏捷性
规则复杂且嵌套业务对象直接代码复杂逻辑更易维护,编译器辅助类型检查
需非技术人员配置规则(产品/运营)表达式引擎降低技术门槛,释放开发资源
性能敏感(如:>10万QPS)直接代码避免表达式解析开销影响吞吐量
多租户定制规则表达式引擎各租户独立配置,互不影响

还是以上面的电商场景为例,让大家感受expr的好处以及使用场景:
场景:电商促销规则判断
需求:根据用户等级、订单金额、商品类目动态调整折扣。

方案一:表达式引擎(expr)

// 规则配置(存储于数据库)
rules := []PromotionRule{
    {
        Condition: `User.Level >= 3 && Order.Amount > 1000 && Order.Category == "electronics"`,
        Discount:  0.85,
    },
}
// 动态执行
valid, _ := ApplyPromotion(user, order, rule)

优势:

  • 运营人员可通过管理后台随时新增/修改规则,无需等待版本发布。
  • 支持A/B测试:为不同用户组配置不同规则。

劣势:

  • 需额外开发规则管理界面和测试工具。

方案二:直接代码判断

func IsPromotionValid(user User, order Order) bool {
    return user.Level >= 3 && 
           order.Amount > 1000 && 
           order.Category == "electronics"
}

优势:

  • 性能极高,适合每秒数十万次调用的场景。
  • 逻辑变更通过代码评审,降低错误风险。

劣势:

  • 修改折扣条件需发版,无法快速响应市场活动。

到此这篇关于Go expr 通用表达式引擎的使用的文章就介绍到这了,更多相关Go expr 通用表达式引擎内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家! 

相关文章

  • 一文带你搞懂Golang结构体内存布局

    一文带你搞懂Golang结构体内存布局

    结构体在Go语言中是一个很重要的部分,在项目中会经常用到。这篇文章主要带大家看一下结构体在内存中是怎么分布的?通过对内存布局的了解,可以帮助我们写出更优质的代码。感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助
    2022-10-10
  • go-cqhttp智能聊天功能的实现

    go-cqhttp智能聊天功能的实现

    这篇文章主要介绍了go-cqhttp智能聊天功能,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-09-09
  • go语言中的json与map相互转换实现

    go语言中的json与map相互转换实现

    本文主要介绍了go语言中的json与map相互转换实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • Golang实践之Error创建和处理详解

    Golang实践之Error创建和处理详解

    在 C#、Java 等语言中常常使用 try...catch的方式来捕获异常,但是在Golang 对于错误处理有不同的方式,像网上也有很多对 error 处理的最佳实践的文章,其中很多其实就是对 error 的统一封装,使用规范进行约束,本文主要是记录自己对处理 Error 的一些认识和学习
    2023-09-09
  • go语言执行windows下命令行的方法

    go语言执行windows下命令行的方法

    这篇文章主要介绍了go语言执行windows下命令行的方法,实例分析了Go语言操作windows下命令行的技巧,需要的朋友可以参考下
    2015-03-03
  • Golang工作池的使用实例讲解

    Golang工作池的使用实例讲解

    我们使用Go语言开发项目,常常会使用到goroutine;goroutine太多会造成系统占用过高或其他系统异常,我们可以将goroutine控制指定数量,且减少goroutine的创建,这就运用到Go工作池,下面就介绍和使用一下
    2023-02-02
  • Go语言编程学习golang配置golint

    Go语言编程学习golang配置golint

    这篇文章主要为大家介绍了Go语言编程学习golang配置golint的过程示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2021-11-11
  • 详解go-zero如何实现令牌桶限流

    详解go-zero如何实现令牌桶限流

    令牌桶算法既能够将所有的请求平均分布到时间区间内,又能接受服务器能够承受范围内的突发请求,因此是目前使用较为广泛的一种限流算法,本文就来看看go-zero如何实现令牌桶限流的吧
    2023-08-08
  • 详解go基于viper实现配置文件热更新及其源码分析

    详解go基于viper实现配置文件热更新及其源码分析

    这篇文章主要介绍了详解go基于viper实现配置文件热更新及其源码分析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-06-06
  • golang 接口嵌套实现复用的操作

    golang 接口嵌套实现复用的操作

    这篇文章主要介绍了golang 接口嵌套实现复用的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-04-04

最新评论