Implementing a Rule Engine in Go with Govaluate
James Reed
Infrastructure Engineer ยท Leapcell

Introduction
In 2024, I used govaluate to write a rule engine. Its advantage lies in endowing Go with the capabilities of a dynamic language, enabling it to perform some calculation operations and obtain results. This allows you to achieve the functionality without writing the corresponding code; instead, you just need to configure a string. It is very suitable for building a rule engine.
Quick Start
First, install it:
$ go get github.com/Knetic/govaluate
Then, use it:
package main import ( "fmt" "log" "github.com/Knetic/govaluate" ) func main() { expr, err := govaluate.NewEvaluableExpression("5 > 0") if err != nil { log.Fatal("syntax error:", err) } result, err := expr.Evaluate(nil) if err != nil { log.Fatal("evaluate error:", err) } fmt.Println(result) }
There are only two steps to calculate an expression using govaluate:
- Call
NewEvaluableExpression()
to convert the expression into an expression object. - Call the
Evaluate
method of the expression object, pass in the parameters, and return the value of the expression.
The above example demonstrates a simple calculation. Using govaluate to calculate the value of 5 > 0
, this expression does not require parameters, so a nil
value is passed to the Evaluate()
method. Of course, this example is not very practical. Obviously, it is more convenient to calculate 5 > 0
directly in the code. However, in some cases, we may not know all the information about the expression that needs to be calculated, and we may not even know the structure of the expression. This is where the role of govaluate becomes prominent.
Parameters
govaluate supports the use of parameters in expressions. When calling the Evaluate()
method of the expression object, parameters can be passed in for calculation through a map[string]interface{}
type. Among them, the key of the map is the parameter name, and the value is the parameter value. For example:
func main() { expr, _ := govaluate.NewEvaluableExpression("foo > 0") parameters := make(map[string]interface{}) parameters["foo"] = -1 result, _ := expr.Evaluate(parameters) fmt.Println(result) expr, _ = govaluate.NewEvaluableExpression("(leapcell_req_made * leapcell_req_succeeded / 100) >= 90") parameters = make(map[string]interface{}) parameters["leapcell_req_made"] = 100 parameters["leapcell_req_succeeded"] = 80 result, _ = expr.Evaluate(parameters) fmt.Println(result) expr, _ = govaluate.NewEvaluableExpression("(mem_used / total_mem) * 100") parameters = make(map[string]interface{}) parameters["total_mem"] = 1024 parameters["mem_used"] = 512 result, _ = expr.Evaluate(parameters) fmt.Println(result) }
In the first expression, we want to calculate the result of foo > 0
. When passing in the parameter, we set foo
to -1, and the final output is false
.
In the second expression, we want to calculate the value of (leapcell_req_made * leapcell_req_succeeded / 100) >= 90
. In the parameters, we set leapcell_req_made
to 100 and leapcell_req_succeeded
to 80, and the result is true
.
The above two expressions both return boolean results, and the third expression returns a floating-point number. (mem_used / total_mem) * 100
returns the memory usage percentage according to the passed-in total memory total_mem
and the current used memory mem_used
, and the result is 50.
Naming
Using govaluate is different from directly writing Go code. In Go code, identifiers cannot contain symbols such as -
, +
, $
, etc. However, govaluate can use these symbols through escaping, and there are two ways of escaping:
- Wrap the name with
[
and]
, for example,[leapcell_resp-time]
. - Use
\
to escape the next character immediately following it.
For example:
func main() { expr, _ := govaluate.NewEvaluableExpression("[leapcell_resp-time] < 100") parameters := make(map[string]interface{}) parameters["leapcell_resp-time"] = 80 result, _ := expr.Evaluate(parameters) fmt.Println(result) expr, _ = govaluate.NewEvaluableExpression("leapcell_resp\\-time < 100") parameters = make(map[string]interface{}) parameters["leapcell_resp-time"] = 80 result, _ = expr.Evaluate(parameters) fmt.Println(result) }
It should be noted that since the \
itself needs to be escaped in a string, \\
should be used in the second expression. Or you can use
`leapcell_resp\-time` < 100
"Compile" Once and Run Multiple Times
Using expressions with parameters, we can achieve "compiling" an expression once and running it multiple times. Just use the expression object returned by the compilation and call its Evaluate()
method multiple times:
func main() { expr, _ := govaluate.NewEvaluableExpression("a + b") parameters := make(map[string]interface{}) parameters["a"] = 1 parameters["b"] = 2 result, _ := expr.Evaluate(parameters) fmt.Println(result) parameters = make(map[string]interface{}) parameters["a"] = 10 parameters["b"] = 20 result, _ = expr.Evaluate(parameters) fmt.Println(result) }
When running for the first time, pass in the parameters a = 1
and b = 2
, and the result is 3; when running for the second time, pass in the parameters a = 10
and b = 20
, and the result is 30.
Functions
If it can only perform regular arithmetic and logical operations, the functionality of govaluate will be greatly reduced. govaluate provides the function of custom functions. All custom functions need to be defined first and stored in a map[string]govaluate.ExpressionFunction
variable, and then call govaluate.NewEvaluableExpressionWithFunctions()
to generate an expression, and these functions can be used in this expression. The type of a custom function is func (args ...interface{}) (interface{}, error)
. If the function returns an error, the evaluation of this expression will also return an error.
func main() { functions := map[string]govaluate.ExpressionFunction{ "strlen": func(args ...interface{}) (interface{}, error) { length := len(args[0].(string)) return length, nil }, } exprString := "strlen('teststring')" expr, _ := govaluate.NewEvaluableExpressionWithFunctions(exprString, functions) result, _ := expr.Evaluate(nil) fmt.Println(result) }
In the above example, we defined a function strlen
to calculate the string length of the first parameter. The expression strlen('teststring')
calls the strlen
function to return the length of the string teststring
.
Functions can accept any number of parameters and can handle the problem of nested function calls. Therefore, complex expressions like the following can be written:
sqrt(x1 ** y1, x2 ** y2)
max(someValue, abs(anotherValue), 10 * lastValue)
Accessors
In the Go language, accessors are used to access fields in a struct through the .
operation. If there is a struct type among the passed-in parameters, govaluate also supports using .
to access its internal fields or call their methods:
type User struct { FirstName string LastName string Age int } func (u User) Fullname() string { return u.FirstName + " " + u.LastName } func main() { u := User{FirstName: "li", LastName: "dajun", Age: 18} parameters := make(map[string]interface{}) parameters["u"] = u expr, _ := govaluate.NewEvaluableExpression("u.Fullname()") result, _ := expr.Evaluate(parameters) fmt.Println("user", result) expr, _ = govaluate.NewEvaluableExpression("u.Age > 18") result, _ = expr.Evaluate(parameters) fmt.Println("age > 18?", result) }
In the above code, we defined a User
struct and wrote a Fullname()
method for it. In the first expression, we call u.Fullname()
to return the full name, and in the second expression, we compare whether the age is greater than 18.
It should be noted that we cannot use the foo.SomeMap['key']
way to access the value of a map. Since accessors involve a lot of reflection, they are usually about 4 times slower than directly using parameters. If you can use the form of parameters, try to use parameters. In the above example, we can directly call u.Fullname()
and pass the result as a parameter to the expression evaluation. Complex calculations can be solved through custom functions. We can also implement the govaluate.Parameter
interface. For unknown parameters used in the expression, govaluate will automatically call its Get()
method to obtain them:
// src/github.com/Knetic/govaluate/parameters.go type Parameters interface { Get(name string) (interface{}, error) }
For example, we can make User
implement the Parameter
interface:
type User struct { FirstName string LastName string Age int } func (u User) Get(name string) (interface{}, error) { if name == "FullName" { return u.FirstName + " " + u.LastName, nil } return nil, errors.New("unsupported field " + name) } func main() { u := User{FirstName: "li", LastName: "dajun", Age: 18} expr, _ := govaluate.NewEvaluableExpression("FullName") result, _ := expr.Eval(u) fmt.Println("user", result) }
The expression object actually has two methods. One is the Evaluate()
method we used before, which accepts a map[string]interface{}
parameter. The other is the Eval()
method we used in this example, which accepts a Parameter
interface. In fact, the Evaluate()
implementation internally also calls the Eval()
method:
// src/github.com/Knetic/govaluate/EvaluableExpression.go func (this EvaluableExpression) Evaluate(parameters map[string]interface{}) (interface{}, error) { if parameters == nil { return this.Eval(nil) } return this.Eval(MapParameters(parameters)) }
When evaluating an expression, the Get()
method of Parameter
needs to be called to obtain unknown parameters. In the above example, we can directly use FullName
to call the u.Get()
method to return the full name.
Supported Operations and Types
The operations and types supported by govaluate are different from those in the Go language. On the one hand, the types and operations in govaluate are not as rich as those in Go; on the other hand, govaluate has also extended some operations.
Arithmetic, Comparison, and Logical Operations
+ - / * & | ^ ** % >> <<
: Addition, subtraction, multiplication, division, bitwise AND, bitwise OR, XOR, exponentiation, modulus, left shift, and right shift.> >= < <= == != =~ !~
:=~
is for regular expression matching, and!~
is for regular expression non-matching.|| &&
: Logical OR and logical AND.
Constants
- Numeric constants. In govaluate, numbers are all treated as 64-bit floating-point numbers.
- String constants. Note that in govaluate, strings are enclosed in single quotes
'
. - Date and time constants. The format is the same as that of strings. govaluate will try to automatically parse whether the string is a date, and only supports limited formats such as RFC3339 and ISO8601.
- Boolean constants:
true
,false
.
Others
- Parentheses can change the calculation precedence.
- Arrays are defined in
()
, and each element is separated by,
. It can support any element type, such as(1, 2, 'foo')
. In fact, in govaluate, arrays are represented by[]interface{}
. - Ternary operator:
? :
.
In the following code, govaluate will first convert 2025-03-02
and 2025-03-01 23:59:59
to the time.Time
type, and then compare their magnitudes:
func main() { expr, _ := govaluate.NewEvaluableExpression("'2025-03-02' > '2025-03-01 23:59:59'") result, _ := expr.Evaluate(nil) fmt.Println(result) }
Error Handling
In the above examples, we deliberately ignored error handling. In fact, govaluate may generate errors in both operations of creating an expression object and evaluating an expression. When generating an expression object, if the expression has a syntax error, an error will be returned. When evaluating an expression, if the passed-in parameters are illegal, or some parameters are missing, or an attempt is made to access a non-existent field in a struct, an error will be reported.
func main() { exprString := `>>>` expr, err := govaluate.NewEvaluableExpression(exprString) if err != nil { log.Fatal("syntax error:", err) } result, err := expr.Evaluate(nil) if err != nil { log.Fatal("evaluate error:", err) } fmt.Println(result) }
We can modify the expression string in turn to verify various errors. First, it is >>>
:
2025/03/19 00:00:00 syntax error:Invalid token: '>>>'
Then we modify it to foo > 0
, but we do not pass in the parameter foo
, and the execution fails:
2025/03/19 00:00:00 evaluate error:No parameter 'foo' found.
Other errors can be verified by yourself.
Conclusion
Although the operations and types supported by govaluate are limited, it can still implement some interesting functions. For example, you can write a web service that allows users to write their own expressions, set parameters, and let the server calculate the results.
Leapcell: The Best of Serverless Web Hosting
Finally, I would like to recommend the most suitable platform for deploying Go services: Leapcell
๐ Build with Your Favorite Language
Develop effortlessly in JavaScript, Python, Go, or Rust.
๐ Deploy Unlimited Projects for Free
Only pay for what you useโno requests, no charges.
โก Pay-as-You-Go, No Hidden Costs
No idle fees, just seamless scalability.
๐ Explore Our Documentation
๐น Follow us on Twitter: @LeapcellHQ