Implementierung einer Regel-Engine in Go mit Govaluate
Wenhao Wang
Dev Intern · Leapcell

Einführung
Im Jahr 2024 habe ich govaluate verwendet, um eine Regel-Engine zu schreiben. Sein Vorteil liegt darin, Go mit den Fähigkeiten einer dynamischen Sprache auszustatten, die es ermöglicht, einige Berechnungsoperationen durchzuführen und Ergebnisse zu erhalten. Dies ermöglicht es Ihnen, die Funktionalität zu erreichen, ohne den entsprechenden Code zu schreiben; stattdessen müssen Sie nur eine Zeichenkette konfigurieren. Es ist sehr gut geeignet, um eine Regel-Engine zu entwickeln.
Schnellstart
Installieren Sie es zuerst:
$ go get github.com/Knetic/govaluate
Dann verwenden Sie es:
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) }
Es gibt nur zwei Schritte, um einen Ausdruck mit govaluate zu berechnen:
- Rufen Sie
NewEvaluableExpression()
auf, um den Ausdruck in ein Ausdrucksobjekt zu konvertieren. - Rufen Sie die Methode
Evaluate
des Ausdrucksobjekts auf, übergeben Sie die Parameter und geben Sie den Wert des Ausdrucks zurück.
Das obige Beispiel demonstriert eine einfache Berechnung. Die Verwendung von govaluate zur Berechnung des Wertes von 5 > 0
, dieser Ausdruck benötigt keine Parameter, daher wird ein nil
-Wert an die Methode Evaluate()
übergeben. Natürlich ist dieses Beispiel nicht sehr praktisch. Offensichtlich ist es bequemer, 5 > 0
direkt im Code zu berechnen. In einigen Fällen kennen wir jedoch möglicherweise nicht alle Informationen über den Ausdruck, der berechnet werden muss, und wir kennen möglicherweise nicht einmal die Struktur des Ausdrucks. Hier wird die Rolle von govaluate deutlich.
Parameter
govaluate unterstützt die Verwendung von Parametern in Ausdrücken. Beim Aufruf der Methode Evaluate()
des Ausdrucksobjekts können Parameter zur Berechnung über einen Typ map[string]interface{}
übergeben werden. Unter diesen ist der Schlüssel der Map der Parametername und der Wert der Parameterwert. Zum Beispiel:
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) }
Im ersten Ausdruck wollen wir das Ergebnis von foo > 0
berechnen. Bei der Übergabe des Parameters setzen wir foo
auf -1, und die endgültige Ausgabe ist false
.
Im zweiten Ausdruck wollen wir den Wert von (leapcell_req_made * leapcell_req_succeeded / 100) >= 90
berechnen. In den Parametern setzen wir leapcell_req_made
auf 100 und leapcell_req_succeeded
auf 80, und das Ergebnis ist true
.
Die obigen beiden Ausdrücke geben beide boolesche Ergebnisse zurück, und der dritte Ausdruck gibt eine Gleitkommazahl zurück. (mem_used / total_mem) * 100
gibt die Speichernutzung in Prozent entsprechend dem übergebenen Gesamtspeicher total_mem
und dem aktuell verwendeten Speicher mem_used
zurück, und das Ergebnis ist 50.
Benennung
Die Verwendung von govaluate unterscheidet sich vom direkten Schreiben von Go-Code. In Go-Code dürfen Bezeichner keine Symbole wie -
, +
, $
usw. enthalten. govaluate kann diese Symbole jedoch durch Escaping verwenden, und es gibt zwei Möglichkeiten des Escapings:
- Schließen Sie den Namen mit
[
und]
ein, zum Beispiel[leapcell_resp-time]
. - Verwenden Sie
\
, um das nächste Zeichen unmittelbar danach zu maskieren.
Zum Beispiel:
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) }
Es ist zu beachten, dass, da das \
selbst in einer Zeichenkette maskiert werden muss, \\
im zweiten Ausdruck verwendet werden sollte. Oder Sie können verwenden
`leapcell_resp\-time` < 100
Einmal "kompilieren" und mehrfach ausführen
Mit Ausdrücken mit Parametern können wir erreichen, dass ein Ausdruck einmal "kompiliert" und mehrfach ausgeführt wird. Verwenden Sie einfach das vom Kompilieren zurückgegebene Ausdrucksobjekt und rufen Sie dessen Evaluate()
-Methode mehrmals auf:
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) }
Wenn Sie es zum ersten Mal ausführen, übergeben Sie die Parameter a = 1
und b = 2
, und das Ergebnis ist 3; Wenn Sie es zum zweiten Mal ausführen, übergeben Sie die Parameter a = 10
und b = 20
, und das Ergebnis ist 30.
Funktionen
Wenn es nur reguläre arithmetische und logische Operationen durchführen kann, wird die Funktionalität von govaluate stark reduziert. govaluate bietet die Funktion von benutzerdefinierten Funktionen. Alle benutzerdefinierten Funktionen müssen zuerst definiert und in einer Variablen map[string]govaluate.ExpressionFunction
gespeichert werden, und dann rufen Sie govaluate.NewEvaluableExpressionWithFunctions()
auf, um einen Ausdruck zu erzeugen, und diese Funktionen können in diesem Ausdruck verwendet werden. Der Typ einer benutzerdefinierten Funktion ist func (args ...interface{}) (interface{}, error)
. Wenn die Funktion einen Fehler zurückgibt, gibt die Auswertung dieses Ausdrucks auch einen Fehler zurück.
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) }
Im obigen Beispiel haben wir eine Funktion strlen
definiert, um die Stringlänge des ersten Parameters zu berechnen. Der Ausdruck strlen('teststring')
ruft die Funktion strlen
auf, um die Länge des Strings teststring
zurückzugeben.
Funktionen können eine beliebige Anzahl von Parametern akzeptieren und das Problem verschachtelter Funktionsaufrufe behandeln. Daher können komplexe Ausdrücke wie die folgenden geschrieben werden:
sqrt(x1 ** y1, x2 ** y2)
max(someValue, abs(anotherValue), 10 * lastValue)
Accessoren
In der Go-Sprache werden Accessoren verwendet, um auf Felder in einer Struktur über die Operation .
zuzugreifen. Wenn unter den übergebenen Parametern ein Strukturtyp vorhanden ist, unterstützt govaluate auch die Verwendung von .
, um auf seine internen Felder zuzugreifen oder ihre Methoden aufzurufen:
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) }
Im obigen Code haben wir eine User
-Struktur definiert und eine Fullname()
-Methode dafür geschrieben. Im ersten Ausdruck rufen wir u.Fullname()
auf, um den vollständigen Namen zurückzugeben, und im zweiten Ausdruck vergleichen wir, ob das Alter größer als 18 ist.
Es ist zu beachten, dass wir nicht die Form foo.SomeMap['key']
verwenden können, um auf den Wert einer Map zuzugreifen. Da Accessoren viel Reflexion beinhalten, sind sie normalerweise etwa 4-mal langsamer als die direkte Verwendung von Parametern. Wenn Sie die Form von Parametern verwenden können, versuchen Sie, Parameter zu verwenden. Im obigen Beispiel können wir direkt u.Fullname()
aufrufen und das Ergebnis als Parameter an die Ausdrucksauswertung übergeben. Komplexe Berechnungen können durch benutzerdefinierte Funktionen gelöst werden. Wir können auch die Schnittstelle govaluate.Parameter
implementieren. Bei unbekannten Parametern, die im Ausdruck verwendet werden, ruft govaluate automatisch seine Get()
-Methode auf, um sie abzurufen:
// src/github.com/Knetic/govaluate/parameters.go type Parameters interface { Get(name string) (interface{}, error) }
Zum Beispiel können wir User
die Schnittstelle Parameter
implementieren lassen:
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) }
Das Ausdrucksobjekt hat eigentlich zwei Methoden. Eine ist die Methode Evaluate()
, die wir zuvor verwendet haben und die einen Parameter map[string]interface{}
akzeptiert. Die andere ist die Methode Eval()
, die wir in diesem Beispiel verwendet haben und die eine Parameter
-Schnittstelle akzeptiert. Tatsächlich ruft die Evaluate()
-Implementierung intern auch die Methode Eval()
auf:
// 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)) }
Bei der Auswertung eines Ausdrucks muss die Methode Get()
von Parameter
aufgerufen werden, um unbekannte Parameter abzurufen. Im obigen Beispiel können wir direkt FullName
verwenden, um die Methode u.Get()
aufzurufen, um den vollständigen Namen zurückzugeben.
Unterstützte Operationen und Typen
Die von govaluate unterstützten Operationen und Typen unterscheiden sich von denen in der Go-Sprache. Einerseits sind die Typen und Operationen in govaluate nicht so reichhaltig wie die in Go; andererseits hat govaluate auch einige Operationen erweitert.
Arithmetische, Vergleichs- und Logische Operationen
+ - / * & | ^ ** % >> <<
: Addition, Subtraktion, Multiplikation, Division, bitweises UND, bitweises ODER, XOR, Potenzierung, Modulo, Linksschieben und Rechtsschieben.> >= < <= == != =~ !~
:=~
dient dem regulären Ausdrucksabgleich und!~
dem regulären Ausdrucksnichtabgleich.|| &&
: Logisches ODER und logisches UND.
Konstanten
- Numerische Konstanten. In govaluate werden Zahlen alle als 64-Bit-Gleitkommazahlen behandelt.
- Stringkonstanten. Beachten Sie, dass in govaluate Strings in einfachen Anführungszeichen
'
eingeschlossen sind. - Datums- und Zeitkonstanten. Das Format ist das gleiche wie bei Strings. govaluate wird versuchen, automatisch zu analysieren, ob der String ein Datum ist, und unterstützt nur eingeschränkte Formate wie RFC3339 und ISO8601.
- Boolesche Konstanten:
true
,false
.
Andere
- Klammern können die Berechnungsreihenfolge ändern.
- Arrays werden in
()
definiert und jedes Element wird durch,
getrennt. Es kann jeden Elementtyp unterstützen, z. B.(1, 2, 'foo')
. Tatsächlich werden Arrays in govaluate durch[]interface{}
dargestellt. - Ternärer Operator:
? :
.
Im folgenden Code konvertiert govaluate zuerst 2025-03-02
und 2025-03-01 23:59:59
in den Typ time.Time
und vergleicht dann ihre Größen:
func main() { expr, _ := govaluate.NewEvaluableExpression("'2025-03-02' > '2025-03-01 23:59:59'") result, _ := expr.Evaluate(nil) fmt.Println(result) }
Fehlerbehandlung
In den obigen Beispielen haben wir die Fehlerbehandlung absichtlich ignoriert. Tatsächlich kann govaluate sowohl bei der Erstellung eines Ausdrucksobjekts als auch bei der Auswertung eines Ausdrucks Fehler erzeugen. Beim Erstellen eines Ausdrucksobjekts wird ein Fehler zurückgegeben, wenn der Ausdruck einen Syntaxfehler aufweist. Bei der Auswertung eines Ausdrucks wird ein Fehler gemeldet, wenn die übergebenen Parameter ungültig sind oder einige Parameter fehlen oder versucht wird, auf ein nicht vorhandenes Feld in einer Struktur zuzugreifen.
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) }
Wir können die Ausdruckszeichenkette nacheinander ändern, um verschiedene Fehler zu überprüfen. Zuerst ist es >>>
:
2025/03/19 00:00:00 syntax error:Invalid token: '>>>'
Dann ändern wir es in foo > 0
, aber wir übergeben nicht den Parameter foo
, und die Ausführung schlägt fehl:
2025/03/19 00:00:00 evaluate error:No parameter 'foo' found.
Andere Fehler können Sie selbst überprüfen.
Fazit
Obwohl die von govaluate unterstützten Operationen und Typen begrenzt sind, kann es dennoch einige interessante Funktionen implementieren. Zum Beispiel können Sie einen Webdienst schreiben, der es Benutzern ermöglicht, ihre eigenen Ausdrücke zu schreiben, Parameter festzulegen und den Server die Ergebnisse berechnen zu lassen.
Leapcell: Das Beste von Serverless Webhosting
Schließlich möchte ich die am besten geeignete Plattform für die Bereitstellung von Go-Diensten empfehlen: Leapcell
🚀 Mit Ihrer Lieblingssprache erstellen
Entwickeln Sie mühelos in JavaScript, Python, Go oder Rust.
🌍 Unbegrenzte Projekte kostenlos bereitstellen
Zahlen Sie nur für das, was Sie nutzen – keine Anfragen, keine Gebühren.
⚡ Pay-as-You-Go, keine versteckten Kosten
Keine Leerlaufgebühren, nur nahtlose Skalierbarkeit.
📖 Entdecken Sie unsere Dokumentation
🔹 Folgen Sie uns auf Twitter: @LeapcellHQ