Common Expression Language (CEL) 是简洁、快速、安全的非图灵完备语言,支持C类语法,适用于轻量级规则评估,可嵌入Go应用实现高效动态逻辑计算。【此简介由AI生成】
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 | ||
| 1 年前 |
通用表达式语言
通用表达式语言(CEL)是一种非图灵完备的语言,其设计理念强调简洁性、执行效率、安全性和跨平台兼容性。CEL 采用类 C 的语法结构,与 C++、Go、Java 及 TypeScript 中的等效表达式几乎如出一辙。
// Check whether a resource name starts with a group name.
resource.name.startsWith("/groups/" + auth.claims.group)
// Determine whether the request is in the permitted time window.
request.time - resource.age < duration("24h")
// Check whether all resource names in a list match a given filter.
auth.claims.email_verified && resources.all(r, r.startsWith(auth.claims.email))
CEL "程序" 是一个单独的表达式。示例代码在 Markdown 中标记为 java、go 和 typescript,以展示语法的通用性。
当完全沙盒化的脚本语言资源消耗过大时,CEL 是进行轻量级表达式求值的理想选择。要快速入门,请尝试 Codelab。
可通过 此仪表盘 查看 cel-go 一致性测试结果。
概述
确定要提供给 CEL 的变量和函数。解析并校验表达式以确保其有效性,然后根据输入对生成的抽象语法树(AST)进行求值。虽然校验是可选的,但强烈建议执行此步骤。
环境配置
让我们通过 cel.Declarations 环境选项向 CEL 暴露 name 和 group 变量:
import "github.com/google/cel-go/cel"
env, err := cel.NewEnv(
cel.Variable("name", cel.StringType),
cel.Variable("group", cel.StringType),
)
至此,环境已准备就绪,可用于解析和类型检查。
CEL 不仅支持所有常见的基本类型,还支持列表、映射,以及对 JSON 和 Protocol Buffers 的一流支持。
解析与检查
解析阶段用于判断表达式在语法上是否有效,并展开环境中存在的所有宏。解析和检查的计算开销比求值更大,因此建议提前完成表达式的解析和检查工作。
为方便起见,解析和检查阶段被合并为 Compile 步骤:
ast, issues := env.Compile(`name.startsWith("/groups/" + group)`)
if issues != nil && issues.Err() != nil {
log.Fatalf("type-check error: %s", issues.Err())
}
prg, err := env.Program(ast)
if err != nil {
log.Fatalf("program construction error: %s", err)
}
解析和检查阶段最终生成的 cel.Program 是无状态的,
线程安全且可缓存。
类型检查是可选的,但强烈推荐进行,它通过静态分析能拒绝部分 语义无效的表达式。此外,检查过程还会生成元数据, 这些元数据可提升函数调用性能及运行时对象字段选择的效率。
宏
宏功能默认启用但可选。引入宏是为了支持某些可选 CEL 特性, 这些特性可能并非所有使用场景都需要,若直接作为核心 CEL 语法, 会带来不必要的语法负担和复杂性。宏在解析阶段展开, 其展开内容会在检查阶段进行类型验证。
例如,启用宏后即可支持有界迭代/折叠操作符。其中 all、exists、
exists_one、filter 和 map 等宏对于针对列表和映射值
执行单一谓词判断尤为实用。
// Ensure all tweets are less than 140 chars
tweets.all(t, t.size() <= 140)
has 宏可用于统一 protobuf 类型与动态(类 JSON)类型中的字段存在性测试逻辑。
// Test whether the field is a non-default value if proto-based, or defined
// in the JSON case.
has(message.field)
传统上,这两种情况都需要在语言层面使用特殊语法,但在 CEL 中,这些功能是通过宏来实现的。
评估
现在,为了乐趣和收益进行评估。评估过程是线程安全且无副作用的。可以向同一个 cel.Program 发送许多不同的输入,如果输入中存在字段但表达式中未引用,这些字段将被忽略。
// The `out` var contains the output of a successful evaluation.
// The `details' var would contain intermediate evaluation state if enabled as
// a cel.ProgramOption. This can be useful for visualizing how the `out` value
// was arrive at.
out, details, err := prg.Eval(map[string]interface{}{
"name": "/groups/acme.co/documents/secret-stuff",
"group": "acme.co"})
fmt.Println(out) // 'true'
部分状态处理
如果未提供 name 会怎样?CEL 正是为此类场景设计的。在分布式应用中,边缘缓存与中心服务并存的情况很常见。理想情况下,表达式应在边缘节点完成求值,但并非总能预先知晓 CEL 表达式中所有值和函数所需的完整状态。
为提高部分状态下的求值成功率,CEL 采用了可交换逻辑运算符 && 和 ||。当左操作数出现错误或未知值(二者不同)时,右操作数仍会被求值以确定最终结果。虽然不依赖此特性也能实现部分状态求值,但该设计既符合 SQL 求值语义,又能更好地应对 JSON 等动态数据类型的求值需求。
以下真值表中,符号 <x> 和 <y> 表示错误或未知值,? 表示因短路求值未执行的分支。当结果为 <x, y> 时,表示两个参数都可能影响结果:
| 表达式 | 结果 |
|---|---|
false && ? |
false |
true && false |
false |
<x> && false |
false |
true && true |
true |
true && <x> |
<x> |
<x> && true |
<x> |
<x> && <y> |
<x, y> |
true || ? |
true |
false || true |
true |
<x> || true |
true |
false || false |
false |
false || <x> |
<x> |
<x> || false |
<x> |
<x> || <y> |
<x, y> |
若预期存在未知值,应启用 cel.EvalOptions(cel.OptTrackState)。Eval() 返回的 details 将包含中间求值结果,可传递给 interpreter.Prune 函数生成剩余表达式。例如:
// Residual when `name` omitted:
name.startsWith("/groups/acme.co")
当某些变量在非必要情况下计算成本较高时,这项技术会非常有用。该功能将成为未来多项改进的重点,敬请期待更多优化!
错误提示
解析与检查错误会提供友好的提示信息,并直接指向源码中问题出现的位置:
ERROR: <input>:1:40: undefined field 'undefined'
| TestAllTypes{single_int32: 1, undefined: 2}
| .......................................^`,
解析和检查后的表达式均包含输出AST中每个节点的源代码位置信息。这些信息同样可用于评估时确定错误位置。
安装
CEL-Go支持modules并采用语义化版本控制。更多信息请参阅Go Modules文档。当然,您也可以选择直接从源码构建。
常见问题
为何不选用JavaScript、Lua或WASM?
JavaScript和Lua是功能丰富的语言,需要沙箱环境才能安全执行。沙箱机制成本高昂,当评估内容复杂度超过O(n)时,会极大影响"允许用户评估什么内容"的决策。
CEL在禁用宏的情况下,其评估耗时与表达式大小及输入数据呈线性关系。除内置函数外,唯一可调用的函数由宿主环境提供。虽然扩展函数可能更复杂,但这取决于嵌入CEL的应用自身的选择。
那么为何不选用WASM?WASM在某些应用中是绝佳选择,远优于嵌入式JavaScript和Lua,但它不支持垃圾回收,且非基本对象类型需要跨模块调用,代价较高。在多数场景中,CEL对其目标用例而言速度更快且同样便携,不过对于node.js和基于Web的执行环境,CEL未来也可能提供直接编译为WASM的评估器。
必须同时进行解析和检查吗?
检查是CEL表达式验证中的可选步骤,但强烈建议执行。某些情况下仅进行解析,依赖运行时绑定和错误处理机制也是可行的。
如何深入了解这门语言?
如何了解内部实现原理?
如何参与贡献?
- 从CONTRIBUTING.md开始入门
- 通过GitHub Issues提交功能请求或报告缺陷
部分测试无法通过go test运行?
少量测试依赖Bazel环境。特别是检查阶段的动态proto支持以及一致性测试驱动程序,需要Bazel协调测试输入:
bazel test ...
许可证
基于 Apache 许可证 发布。
免责声明:此非 Google 官方产品。