1. Introduction
Expr is a dynamic configuration solution designed for the Go language, known for its simple syntax and powerful performance features. The core of the Expr expression engine is focused on safety, speed, and intuitiveness, making it suitable for scenarios such as access control, data filtering, and resource management. When applied to Go, Expr greatly enhances the ability of applications to handle dynamic rules. Unlike interpreters or script engines in other languages, Expr adopts static type checking and generates bytecode for execution, ensuring both performance and security.
2. Installing Expr
You can install the Expr expression engine using the Go language's package management tool go get
:
go get github.com/expr-lang/expr
This command will download the Expr library files and install them into your Go project, allowing you to import and use Expr in your Go code.
3. Quick Start
3.1 Compiling and Running Basic Expressions
Let's start with a basic example: writing a simple expression, compiling it, and then running it to obtain the result.
package main
import (
"fmt"
"github.com/expr-lang/expr"
)
func main() {
// Compiling a basic addition expression
program, err := expr.Compile(`2 + 2`)
if err != nil {
panic(err)
}
// Running the compiled expression without passing an environment, as no variables are needed here
output, err := expr.Run(program, nil)
if err != nil {
panic(err)
}
// Printing the result
fmt.Println(output) // Outputs 4
}
In this example, the expression 2 + 2
is compiled into executable bytecode, which is then executed to produce the output.
3.2 Using Variable Expressions
Next, we'll create an environment containing variables, write an expression that uses these variables, compile and run this expression.
package main
import (
"fmt"
"github.com/expr-lang/expr"
)
func main() {
// Creating an environment with variables
env := map[string]interface{}{
"foo": 100,
"bar": 200,
}
// Compiling an expression that uses variables from the environment
program, err := expr.Compile(`foo + bar`, expr.Env(env))
if err != nil {
panic(err)
}
// Running the expression
output, err := expr.Run(program, env)
if err != nil {
panic(err)
}
// Printing the result
fmt.Println(output) // Outputs 300
}
In this example, the environment env
contains variables foo
and bar
. The expression foo + bar
infers the types of foo
and bar
from the environment during compilation, and uses the values of these variables at runtime to evaluate the expression result.
4. Expr Syntax in Detail
4.1 Variables and Literals
The Expr expression engine can handle common data type literals, including numbers, strings, and boolean values. Literals are data values directly written in the code, such as 42
, "hello"
, and true
.
Numbers
In Expr, you can directly write integers and floating point numbers:
42 // Represents the integer 42
3.14 // Represents the floating point number 3.14
Strings
String literals are enclosed in double quotes "
or backticks ``. For example:
"hello, world" // String enclosed in double quotes, supports escape characters
`hello, world` // String enclosed in backticks, maintains the string format without support for escape characters
Booleans
There are only two boolean values, true
and false
, representing logical true and false:
true // Boolean true value
false // Boolean false value
Variables
Expr also allows the definition of variables in the environment, and then references these variables in the expression. For example:
env := map[string]interface{}{
"age": 25,
"name": "Alice",
}
Then in the expression, you can refer to age
and name
:
age > 18 // Check if age is greater than 18
name == "Alice" // Determine if name is equal to "Alice"
4.2 Operators
The Expr expression engine supports various operators, including arithmetic operators, logical operators, comparison operators, and set operators, etc.
Arithmetic and Logical Operators
Arithmetic operators include addition (+
), subtraction (-
), multiplication (*
), division (/
), and modulo (%
). Logical operators include logical AND (&&
), logical OR (||
), and logical NOT (!
), for example:
2 + 2 // Result is 4
7 % 3 // Result is 1
!true // Result is false
age >= 18 && name == "Alice" // Check if age is not less than 18 and if name is equal to "Alice"
Comparison Operators
Comparison operators include equal to (==
), not equal to (!=
), less than (<
), less than or equal to (<=
), greater than (>
), and greater than or equal to (>=
), used to compare two values:
age == 25 // Check if age is equal to 25
age != 18 // Check if age is not equal to 18
age > 20 // Check if age is greater than 20
Set Operators
Expr also provides some operators for working with sets, such as in
to check if an element is in the set. Sets can be arrays, slices, or maps:
"user" in ["user", "admin"] // true, because "user" is in the array
3 in {1: true, 2: false} // false, because 3 is not a key in the map
There are also some advanced set operation functions, such as all
, any
, one
, and none
, which require the use of anonymous functions (lambda):
all(tweets, {.Len <= 240}) // Check if the Len field of all tweets does not exceed 240
any(tweets, {.Len > 200}) // Check if there exists a Len field in tweets that exceeds 200
Member Operator
In the Expr expression language, the member operator allows us to access the properties of the struct
in Go language. This feature enables Expr to directly manipulate complex data structures, making it very flexible and practical.
Using the member operator is very simple, just use the .
operator followed by the property name. For example, if we have the following struct
:
type User struct {
Name string
Age int
}
You can write an expression to access the Name
property of the User
structure like this:
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) // Output: Alice
Handling nil Values
When accessing properties, you may encounter situations where the object is nil
. Expr provides safe property access, so even if the struct or nested property is nil
, it will not throw a runtime panic error.
Use the ?.
operator to reference properties. If the object is nil, it will return nil instead of throwing an error.
author.User?.Name
Equivalent expression
author.User != nil ? author.User.Name : nil
Using the ??
operator is primarily for returning default values:
author.User?.Name ?? "Anonymous"
Equivalent expression
author.User != nil ? author.User.Name : "Anonymous"
Pipe Operator
The pipe operator (|
) in Expr is used to pass the result of one expression as a parameter to another expression. This is similar to the pipe operation in Unix shell, allowing multiple functional modules to be chained together to form a processing pipeline. In Expr, using it can create more clear and concise expressions.
For example, if we have a function to get a user's name and a template for a welcome message:
env := map[string]interface{}{
"user": User{Name: "Bob", Age: 30},
"get_name": func(u User) string { return u.Name },
"greet_msg": "Hello, %s!",
}
code := `get_name(user) | sprintf(greet_msg)`
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) // Output: Hello, Bob!
In this example, we first get the user's name through get_name(user)
, then pass the name to the sprintf
function using the pipe operator |
to generate the final welcome message.
Using the pipe operator can modularize our code, improve code reusability, and make the expressions more readable.
4.3 Functions
Expr supports built-in functions and custom functions, making expressions more powerful and flexible.
Using Built-in Functions
Built-in functions like len
, all
, none
, any
, etc. can be used directly in the expression.
// Example of using a built-in function
program, err := expr.Compile(`all(users, {.Age >= 18})`, expr.Env(env))
if err != nil {
panic(err)
}
// Note: here the env needs to contain the users variable, and each user must have the Age property
output, err := expr.Run(program, env)
fmt.Print(output) // If all users in env are aged 18 or above, it will return true
How to define and use custom functions
In Expr, you can create custom functions by passing function definitions in the environment mapping.
// Custom function example
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) // Returns Hello, World!
When using functions in Expr, you can modularize your code and incorporate complex logic into the expressions. By combining variables, operators, and functions, Expr becomes a powerful and easy-to-use tool. Remember to always ensure type safety when building the Expr environment and running expressions.
5. Built-in Function Documentation
Expr expression engine provides developers with a rich set of built-in functions to handle various complex scenarios. Below, we will detail these built-in functions and their usage.
all
The all
function can be used to check if all elements in a collection satisfy a given condition. It takes two parameters: the collection and the condition expression.
// Checking if all tweets have a content length less than 240
code := `all(tweets, len(.Content) < 240)`
any
Similar to all
, the any
function is used to check if any element in a collection satisfies a condition.
// Checking if any tweet has a content length greater than 240
code := `any(tweets, len(.Content) > 240)`
none
The none
function is used to check if no element in a collection satisfies a condition.
// Ensuring no tweets are repeated
code := `none(tweets, .IsRepeated)`
one
The one
function is used to confirm that only one element in a collection satisfies a condition.
// Checking if only one tweet contains a specific keyword
code := `one(tweets, contains(.Content, "keyword"))`
filter
The filter
function can filter out collection elements that satisfy a given condition.
// Filtering out all tweets marked as priority
code := `filter(tweets, .IsPriority)`
map
The map
function is used to transform elements in a collection according to a specified method.
// Formatting the publish time of all tweets
code := `map(tweets, {.PublishTime: Format(.Date)})`
len
The len
function is used to return the length of a collection or string.
// Getting the length of the username
code := `len(username)`
contains
The contains
function is used to check whether a string contains a substring or if a collection contains a specific element.
// Checking if the username contains illegal characters
code := `contains(username, "illegal characters")`
The ones mentioned above are just a part of the built-in functions provided by the Expr expression engine. With these powerful functions, you can handle data and logic more flexibly and efficiently. For a more detailed list of functions and usage instructions, please refer to the official Expr documentation.