goval
该库允许程序评估任意算术/字符串/逻辑表达式。 支持访问变量和调用自定义函数。
该项目被认为是稳定的,并已在生产系统中使用。 请使用问题跟踪器提出任何问题、反馈和错误报告。
本项目根据MIT许可证的条款进行许可。
演示
在示例文件夹中可以找到一个评估表达式的小型CLI演示:
go get -u github.com/maja42/goval
cd $GOPATH/src/github.com/maja42/goval/
go run example/main.go
用法
最小示例:
eval := goval.NewEvaluator()
result, err := eval.Evaluate(`42 > 21`, nil, nil) // 返回 <true, nil>
访问变量:
eval := goval.NewEvaluator()
variables := map[string]interface{}{
"uploaded": 146,
"total": 400,
}
result, err := eval.Evaluate(`uploaded * 100 / total`, variables, nil) // 返回 <36, nil>
调用函数:
// 实现 strlen()
eval := goval.NewEvaluator()
variables := map[string]interface{}{
"os": runtime.GOOS,
"arch": runtime.GOARCH,
}
functions := make(map[string]goval.ExpressionFunction)
functions["strlen"] = func(args ...interface{}) (interface{}, error) {
str := args[0].(string)
return len(str), nil
}
result, err := eval.Evaluate(`strlen(arch[:2]) + strlen("text")`, variables, functions) // 返回 <6, nil>
自定义函数允许扩展任意功能,如正则表达式匹配:
// 实现正则表达式(省略错误处理)
functions := make(map[string]goval.ExpressionFunction)
functions["matches"] = func(args ...interface{}) (interface{}, error) {
str := args[0].(string)
exp := args[1].(string)
reg := regexp.MustCompile(exp)
return reg.MatchString(str), nil
}
eval.Evaluate(`matches("text", "[a-z]+")`, nil, functions) // 返回 <true, nil>
eval.Evaluate(`matches("1234", "[a-z]+")`, nil, functions) // 返回 <false, nil>
文档
类型
该库完全支持以下类型:nil
、bool
、int
、float64
、string
、[]interface{}
(=数组) 和 map[string]interface{}
(=对象)。
在表达式中,int
和 float64
都具有 number
类型,并且完全透明。
如果需要,数值将在 int
和 float64
之间自动转换,只要不损失精度。
数组和对象是无类型的。它们可以存储任何其他值("混合数组")。
不支持结构体,以保持功能清晰和可管理。 它们会引入太多边缘情况和未解决的问题,因此不在范围内。
变量
可以直接访问自定义变量。 变量是只读的,不能在表达式内修改。
示例:
var
var.field
var[0]
var["field"]
var[anotherVar]
var["fie" + "ld"].field[42 - var2][0]
函数
可以在表达式中调用自定义函数。
示例:
rand()
floor(42)
min(4, 3, 12, max(1, 3, 3))
len("te" + "xt")
字面量
可以在表达式中定义任何字面量。
字符串字面量可以用双引号 "
或反引号 括起来。 十六进制字面量以
0x` 前缀开始。
示例:
nil
true
false
3
3.2
"Hello, 世界!\n"
"te\"xt"
`te"xt`
[0, 1, 2]
[]
[0, ["text", false], 4.2]
{}
{"a": 1, "b": {c: 3}}
{"key" + 42: "value"}
{"k" + "e" + "y": "value"}
0xA // 10
0x0A // 10
0xFF // 255
0xFFFFFFFF // 32位应用:-1 64位应用:4294967295
0xFFFFFFFFFFFFFFFF // 64位应用:-1 32位应用:错误
可以访问数组和对象字面量的元素:
示例:
[1, 2, 3][1] // 2
[1, [2, 3, 42][1][2] // 42
{"a": 1}.a // 1
{"a": {"b": 42}}.a.b // 42
{"a": {"b": 42}}["a"]["b"] // 42
优先级
运算符优先级严格遵循 C/C++ 规则。
括号 ()
用于控制优先级。
示例:
1 + 2 * 3 // 7
(1 + 2) * 3 // 9
运算符
算术
算术 +
-
*
/
如果两边都是整数,结果值也是整数。 否则,结果将是浮点数。
示例:
3 + 4 // 7
2 + 2 * 3 // 8
2 * 3 + 2.5 // 8.5
12 - 7 - 5 // 0
24 / 10 // 2
24.0 / 10 // 2.4
幂 **
如果两边都是整数,并且结果可以表示为整数,则结果值也是整数。 否则,结果将是浮点数。
示例:
2 ** 4 // 16
10 ** 3 // 1000
10 ** -6 // 0.000001
2.5 ** -2.5 // 0.10119...
取模 %
如果两边都是整数,结果值也是整数。 否则,结果将是浮点数。
示例:
4 % 3 // 1
144 % 85 // -55
5.5 % 2 // 1.5
10 % 3.5 // 3.0
取反 -
(一元减号)
对右侧的数字取反。
示例:
-4 // -4
5 + -4 // 1
-5 - -4 // -1
1 + --1 // 语法错误
-(4+3) // -7
-varName
连接
字符串连接 +
如果 +
运算符的左侧或右侧是 string
,则执行字符串连接。
支持字符串、数字、布尔值和 nil。
示例:
"text" + 42 // "text42"
"text" + 4.2 // "text4.2"
42 + "text" // "42text"
"text" + nil // "textnil"
"text" + true // "texttrue"
数组连接 +
如果 +
运算符的两侧都是数组,它们将被连接
示例:
[0, 1] + [2, 3] // [0, 1, 2, 3]
[0] + [1] + [[2]] + [] // [0, 1, [2]]
对象连接 +
如果 +
运算符的两侧都是对象,它们的字段将被合并到一个新对象中。
如果两个对象包含相同的键,右侧对象的值将覆盖左侧对象的值。
示例:
{"a": 1} + {"b": 2} + {"c": 3} // {"a": 1, "b": 2, "c": 3}
{"a": 1, "b": 2} + {"b": 3, "c": 4} // {"a": 1, "b": 3, "c": 4}
{"b": 3, "c": 4} + {"a": 1, "b": 2} // {"a": 1, "b": 2, "c": 4}
逻辑
等于 ==
,不等于 !=
对两个操作数执行深度比较。
在比较 int
和 float64
时,
整数将被转换为浮点数。
比较 <
、>
、<=
、>=
比较两个数字。如果运算符的一侧是整数,另一侧是浮点数, 整数将被转换。这可能会导致非常大的数字在转换过程中被舍入,从而产生意外结果。
示例:
3 <-4 // false
45 > 3.4 // false
-4 <= -1 // true
3.5 >= 3.5 // true
与 &&
,或 ||
示例:
true && true // true
false || false // false
true || false && false // true
false && false || true // true
非 !
对右侧的布尔值取反。
示例:
!true // false
!false // true
!!true // true
!varName
三元运算符 ? :
如果表达式解析为 true
,运算符解析为左操作数。
如果表达式解析为 false
,运算符解析为右操作数。
示例:
true ? 1 : 2 // 1
false ? 1 : 2 // 2
2 < 5 ? "a" : 1.5 // "a"
9 > 12 ? "a" : [42] // [42]
false ? (true ? 1:2) : (true ? 3:4) // 3
请注意,所有操作数都会被解析(没有短路)。
在以下示例中,两个函数都会被调用(func2
的返回值简单地被忽略):
true ? func1() : func2()
位操作
逻辑或 |
,逻辑与 &
,逻辑异或 ^
如果运算符的一侧是浮点数,则如果可能会将该数字转换为整数。 如果在该过程中会丢失小数位,则认为是类型错误。 结果数字始终是整数。
示例:
8 | 2 // 10
9 | 5 // 13
8 | 2.0 // 10
8 | 2.1 // 类型错误
13 & 10 // 8
10 & 15.0 & 2 // 2
13 ^ 10 // 7
10 ^ 15 ^ 1 // 4
按位非 ~
如果对浮点数执行,则如果可能会将该数字转换为整数。 如果在该过程中会丢失小数位,则认为是类型错误。 结果数字始终是整数。
结果可能因32位和64位架构而异。
示例:
~-1 // 0
(~0xA55A) & 0xFFFF // 0x5AA5
(~0x5AA5) & 0xFFFF // 0xA55A
~0xFFFFFFFF // 64位应用: 0xFFFFFFFF 00000000; 32位应用: 0x00
~0xFFFFFFFF FFFFFFFF // 64位应用: 0x00; 32位: 错误
位移 <<
, >>
如果运算符的一侧是浮点数,则如果可能会将该数字转换为整数。 如果在该过程中会丢失小数位,则认为是类型错误。 结果数字始终是整数。
右移时执行符号扩展。 结果可能因32位和64位架构而异。
示例:
1 << 0 // 1
1 << 1 // 2
1 << 2 // 4
8 << -1 // 4
8 >> -1 // 16
1 << 31 // 0x00000000 80000000 64位应用: 2147483648; 32位应用: -2147483648
1 << 32 // 0x00000001 00000000 32位应用: 0 (溢出)
1 << 63 // 0x80000000 00000000 32位应用: 0 (溢出); 64位应用: -9223372036854775808
1 << 64 // 0x00000000 00000000 0 (溢出)
0x80000000 00000000 >> 63 // 0xFFFFFFFF FFFFFFFF 64位: -1 (符号扩展); 32位: 错误 (无法解析数字字面量)
0x80000000 >> 31 // 64位: 0x00000000 0000001; 32位: 0xFFFFFFFF (-1, 符号扩展)
更多
数组包含 in
返回数组是否包含特定元素的布尔值。
示例:
"txt" in [nil, "hello", "txt", 42] // true
true in [nil, "hello", "txt", 42] // false
nil in [nil, "hello", "txt", 42] // true
42.0 in [nil, "hello", "txt", 42] // true
2 in [1, [2, 3], 4] // false
[2, 3] in [1, [2, 3], 4] // true
[2, 3, 4] in [1, [2, 3], 4] // false
子字符串 [a:b]
切片字符串并返回给定的子字符串。 字符串按字节索引。多字节字符需要小心处理。
开始索引表示子字符串中存在的第一个字节。
结束索引表示子字符串中不存在的最后一个字节。
因此,有效索引范围为 [0, len(str)]
。
示例:
"abcdefg"[:] // "abcdefg"
"abcdefg"[1:] // "bcdefg"
"abcdefg"[:6] // "abcdef"
"abcdefg"[2:5] // "cde"
"abcdefg"[3:4] // "d"
// 字符 世 和 界 都需要3个字节:
"Hello, 世界"[7:13] // "世界"
"Hello, 世界"[7:10] // "世"
"Hello, 世界"[10:13] // "界"
数组切片 [a:b]
切片数组并返回给定的子数组。
开始索引表示子数组中存在的第一个元素。
结束索引表示子数组中不存在的最后一个元素。
因此,有效索引范围为 [0, len(arr)]
。
示例:
// 假设 `arr := [0, 1, 2, 3, 4, 5, 6]`:
arr[:] // [0, 1, 2, 3, 4, 5, 6]
arr[1:] // [1, 2, 3, 4, 5, 6]
arr[:6] // [0, 1, 2, 3, 4, 5]
arr[2:5] // [2, 3, 4]
arr[3:4] // [3]
替代库
如果你正在寻找通用评估库, 你也可以看看 Knetic/govaluate。 我自己使用过该库,但由于存在几个缺点,我决定创建 goval。 主要区别是:
- 更直观的语法
- 没有中间 AST - 评估和解析在一个步骤中进行
- 更好的类型支持:
- 对数组和对象的完全支持。
- 在
int
和float64
之间不透明的区分。
只要不丢失精度,底层类型就会自动转换。 - 类型感知的位操作(它们只适用于
int
类型的数字)。 - 不支持日期(字符串就是字符串,它们没有特殊含义,即使它们看起来像日期)。
如果需要,可以添加对日期和结构体的支持。
- 更多运算符:
- 通过
.
和[]
语法访问变量(映射) - 支持数组和对象的连接。
- 切片和子字符串
- 通过
- 十六进制字面量(一旦涉及位操作就很有用)。
- 带有
[]
的数组字面量以及带有{}
的对象字面量 - 有用的错误消息。
- 通过使用 go/scanner 和 goyacc 高度优化的解析器代码。
这导致代码大小大大减小(因此错误可能性很小) 并创建超快的代码。 - 高测试覆盖率(包括许多特殊情况)。
还在 32 位和 64 位架构上进行了测试,其中一些(记录的)操作如按位非可能会根据int
的大小而表现不同。