Skip to main content
Version: v1.16

Getting Started

Expr is a simple, fast and extensible expression language for Go. It is designed to be easy to use and integrate into your Go application. Let's delve deeper into its core features:

  • Memory safe - Designed to prevent vulnerabilities like buffer overflows and memory leaks.
  • Type safe - Enforces strict type rules, aligning with Go's type system.
  • Terminating - Ensures every expression evaluation cannot loop indefinitely.
  • Side effects free - Evaluations won't modify global states or variables.

Let's start with a simple example:

program, err := expr.Compile(`2 + 2`)
if err != nil {
panic(err)
}

output, err := expr.Run(program, nil)
if err != nil {
panic(err)
}

fmt.Print(output) // 4

Expr compiles the expression 2 + 2 into a bytecode program. Then we run the program and get the output.

tip

In performance-critical applications, you can reuse the compiled program. Compiled programs are safe for concurrent use. Compile once and run multiple times.

The expr.Compile function returns a *vm.Program and an error. The expr.Run function takes a program and an environment. The environment is a map of variables that can be used in the expression. In this example, we use nil as an environment because we don't need any variables.

Now let's pass some variables to the expression:

env := map[string]any{
"foo": 100,
"bar": 200,
}

program, err := expr.Compile(`foo + bar`, expr.Env(env))
if err != nil {
panic(err)
}

output, err := expr.Run(program, env)
if err != nil {
panic(err)
}

fmt.Print(output) // 300

Why do we need to pass the environment to the expr.Compile function? Expr can be used as a type-safe language. Expr can infer the type of the expression and check it against the environment.

Here is an example:

env := map[string]any{
"name": "Anton",
"age": 35,
}

program, err := expr.Compile(`name + age`, expr.Env(env))
if err != nil {
panic(err) // Will panic with "invalid operation: string + int"
}

Expr can work with any Go types:

env := map[string]any{
"greet": "Hello, %v!",
"names": []string{"world", "you"},
"sprintf": fmt.Sprintf,
}

program, err := expr.Compile(`sprintf(greet, names[0])`, expr.Env(env))
if err != nil {
panic(err)
}

output, err := expr.Run(program, env)
if err != nil {
panic(err)
}

fmt.Print(output) // Hello, world!

Also, Expr can use a struct as an environment. Here is an example:

type Env struct {
Posts []Post `expr:"posts"`
}

func (Env) Format(t time.Time) string { // Methods defined on the struct become functions.
return t.Format(time.RFC822)
}

type Post struct {
Body string
Date time.Time
}

func main() {
code := `map(posts, Format(.Date) + ": " + .Body)`

program, err := expr.Compile(code, expr.Env(Env{})) // Pass the struct as an environment.
if err != nil {
panic(err)
}

env := Env{
Posts: []Post{
{"Oh My God!", time.Now()},
{"How you doin?", time.Now()},
{"Could I be wearing any more clothes?", time.Now()},
},
}

output, err := expr.Run(program, env)
if err != nil {
panic(err)
}

fmt.Print(output)
}

The compiled program can be reused between runs.

type Env struct {
X int
Y int
}

program, err := expr.Compile(`X + Y`, expr.Env(Env{}))
if err != nil {
panic(err)
}

output, err := expr.Run(program, Env{1, 2})
if err != nil {
panic(err)
}

fmt.Print(output) // 3

output, err = expr.Run(program, Env{3, 4})
if err != nil {
panic(err)
}

fmt.Print(output) // 7
Eval = Compile + Run

For one-off expressions, you can use the expr.Eval function. It compiles and runs the expression in one step.

output, err := expr.Eval(`2 + 2`, env)