Go でルールエンジンを実装する Govaluate
Wenhao Wang
Dev Intern · Leapcell

はじめに
2024年、私は govaluate を使用してルールエンジンを作成しました。その利点は、Go に動的言語の機能を与え、いくつかの計算操作を実行して結果を取得できるようにすることです。これにより、対応するコードを記述せずに機能を実現できます。代わりに、文字列を構成するだけで済みます。ルールエンジンの構築に非常に適しています。
クイックスタート
まず、インストールします。
$ go get github.com/Knetic/govaluate
次に、それを使用します。
package main import ( "fmt" "log" "github.com/Knetic/govaluate" ) func main() { expr, err := govaluate.NewEvaluableExpression("5 > 0") if err != nil { log.Fatal("構文エラー: ", err) } result, err := expr.Evaluate(nil) if err != nil { log.Fatal("評価エラー: ", err) } fmt.Println(result) }
govaluate を使用して式を計算するには、次の 2 つの手順しかありません。
NewEvaluableExpression()
を呼び出して、式を式オブジェクトに変換します。- 式オブジェクトの
Evaluate
メソッドを呼び出し、パラメータを渡し、式の値を返します。
上記の例は、簡単な計算を示しています。 govaluate を使用して 5 > 0
の値を計算する場合、この式はパラメータを必要としないため、nil
値が Evaluate()
メソッドに渡されます。 もちろん、この例はあまり実用的ではありません。 明らかに、コードで 5 > 0
を直接計算する方が便利です。 ただし、場合によっては、計算する必要がある式に関するすべての情報がわからなかったり、式の構造さえわからない場合があります。 ここで、govaluate の役割が顕著になります。
パラメータ
govaluate は、式でのパラメータの使用をサポートしています。 式オブジェクトの Evaluate()
メソッドを呼び出す場合、map[string]interface{}
型を介して計算用のパラメータを渡すことができます。 中でも、マップのキーはパラメータ名、値はパラメータ値です。 例:
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) }
最初の式では、foo > 0
の結果を計算したいと考えています。 パラメータを渡すときに、foo
を -1 に設定すると、最終的な出力は false
になります。
2 番目の式では、(leapcell_req_made * leapcell_req_succeeded / 100) >= 90
の値を計算したいと考えています。 パラメータでは、leapcell_req_made
を 100 に、leapcell_req_succeeded
を 80 に設定すると、結果は true
になります。
上記の 2 つの式はどちらもブール値を返し、3 番目の式は浮動小数点数を返します。 (mem_used / total_mem) * 100
は、渡された総メモリ total_mem
と現在の使用メモリ mem_used
に従ってメモリ使用率のパーセンテージを返し、結果は 50 です。
命名
govaluate の使用は、Go コードを直接記述するのとは異なります。 Go コードでは、識別子に -
、+
、$
などの記号を含めることはできません。 ただし、govaluate はエスケープを使用してこれらの記号を使用できます。エスケープには 2 つの方法があります。
- 名前を
[
と]
で囲みます (例:[leapcell_resp-time]
)。 \
を使用して、直後の次の文字をエスケープします。
例:
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) }
\
自体が文字列でエスケープされる必要があるため、2 番目の式では \\
を使用する必要があることに注意してください。 または、次を使用できます
`leapcell_resp\-time` < 100
「コンパイル」を一度行い、複数回実行する
パラメータ付きの式を使用すると、式を「コンパイル」して複数回実行できます。 コンパイルによって返された式オブジェクトを使用し、その Evaluate()
メソッドを複数回呼び出すだけです。
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) }
最初に実行する場合、パラメータ a = 1
と b = 2
を渡すと、結果は 3 になります。2 回目に実行する場合、パラメータ a = 10
と b = 20
を渡すと、結果は 30 になります。
関数
通常の算術演算と論理演算のみを実行できる場合、govaluate の機能は大幅に低下します。 govaluate はカスタム関数の機能を提供します。 すべてのカスタム関数は、最初に定義して map[string]govaluate.ExpressionFunction
変数に保存し、次に govaluate.NewEvaluableExpressionWithFunctions()
を呼び出して式を生成する必要があります。これらの関数はこの式で使用できます。 カスタム関数の型は func (args ...interface{}) (interface{}, 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) }
上記の例では、最初のパラメータの文字列長を計算する関数 strlen
を定義しました。 式 strlen('teststring')
は strlen
関数を呼び出して、文字列 teststring
の長さを返します。
関数は任意の数のパラメータを受け入れることができ、ネストされた関数呼び出しの問題を処理できます。 したがって、次のような複雑な式を記述できます。
sqrt(x1 ** y1, x2 ** y2)
max(someValue, abs(anotherValue), 10 * lastValue)
アクセサー
Go 言語では、アクセサーは .
演算を使用して構造体のフィールドにアクセスするために使用されます。 渡されたパラメータの中に struct 型がある場合、govaluate は .
を使用して内部フィールドにアクセスしたり、それらのメソッドを呼び出したりすることもサポートしています。
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) }
上記のコードでは、User
struct を定義し、その Fullname()
メソッドを作成しました。 最初の式では、u.Fullname()
を呼び出してフルネームを返し、2 番目の式では、年齢が 18 より大きいかどうかを比較します。
foo.SomeMap['key']
の方法を使用してマップの値にアクセスできないことに注意してください。 アクセサーは多くのリフレクションを含むため、通常、パラメータを直接使用するよりも約 4 倍遅くなります。 パラメータの形式を使用できる場合は、パラメータを使用してみてください。 上記の例では、u.Fullname()
を直接呼び出し、結果をパラメータとして式評価に渡すことができます。 複雑な計算は、カスタム関数を使用して解決できます。 また、govaluate.Parameter
インターフェースを実装することもできます。 式で使用される不明なパラメータの場合、govaluate は自動的にその Get()
メソッドを呼び出して取得します。
// src/github.com/Knetic/govaluate/parameters.go type Parameters interface { Get(name string) (interface{}, error) }
たとえば、User
に Parameter
インターフェースを実装させることができます。
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("サポートされていないフィールド " + name) } func main() { u := User{FirstName: "li", LastName: "dajun", Age: 18} expr, _ := govaluate.NewEvaluableExpression("FullName") result, _ := expr.Eval(u) fmt.Println("user", result) }
式オブジェクトには、実際には 2 つのメソッドがあります。 1 つは、以前に使用した Evaluate()
メソッドで、map[string]interface{}
パラメータを受け入れます。 もう 1 つは、この例で使用した Eval()
メソッドで、Parameter
インターフェースを受け入れます。 実際、Evaluate()
の実装は内部的に Eval()
メソッドも呼び出します。
// 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)) }
式を評価する場合、Parameter
の Get()
メソッドを呼び出して不明なパラメータを取得する必要があります。 上記の例では、FullName
を直接使用して u.Get()
メソッドを呼び出し、フルネームを返すことができます。
サポートされている操作と型
govaluate でサポートされている操作と型は、Go 言語の操作と型とは異なります。 一方で、govaluate の型と操作は Go ほど豊富ではありません。 一方、govaluate はいくつかの操作も拡張しました。
算術演算、比較演算、論理演算
+ - / * & | ^ ** % >> <<
: 加算、減算、乗算、除算、ビット単位 AND、ビット単位 OR、XOR、指数、剰余、左シフト、右シフト。> >= < <= == != =~ !~
:=~
は正規表現マッチング用、!~
は正規表現非マッチング用です。|| &&
: 論理 OR と論理 AND。
定数
- 数値定数。 govaluate では、数値はすべて 64 ビット浮動小数点数として扱われます。
- 文字列定数。 govaluate では、文字列は一重引用符
'
で囲まれていることに注意してください。 - 日付と時刻の定数。 形式は文字列の場合と同じです。 govaluate は、文字列が日付かどうかを自動的に解析しようとし、RFC3339 や ISO8601 などの限られた形式のみをサポートします。
- ブール定数:
true
、false
。
その他
- 括弧は計算の優先順位を変更できます。
- 配列は
()
で定義され、各要素は,
で区切られます。(1, 2, 'foo')
など、任意の要素型をサポートできます。 実際、govaluate では、配列は[]interface{}
で表されます。 - 三項演算子:
? :
。
次のコードでは、govaluate は最初に 2025-03-02
と 2025-03-01 23:59:59
を time.Time
型に変換し、それらの大きさを比較します。
func main() { expr, _ := govaluate.NewEvaluableExpression("'2025-03-02' > '2025-03-01 23:59:59'") result, _ := expr.Evaluate(nil) fmt.Println(result) }
エラー処理
上記の例では、エラー処理を意図的に無視しました。 実際、govaluate は、式オブジェクトの作成と式の評価の両方の操作でエラーを生成する可能性があります。 式オブジェクトを生成する場合、式に構文エラーがある場合は、エラーが返されます。 式を評価する場合、渡されたパラメータが不正であるか、一部のパラメータが欠落しているか、または構造体内の存在しないフィールドにアクセスしようとすると、エラーが報告されます。
func main() { exprString := `>>>` expr, err := govaluate.NewEvaluableExpression(exprString) if err != nil { log.Fatal("構文エラー: ", err) } result, err := expr.Evaluate(nil) if err != nil { log.Fatal("評価エラー: ", err) } fmt.Println(result) }
式文字列を順番に変更して、さまざまなエラーを確認できます。 まず、>>>
です。
2025/03/19 00:00:00 構文エラー: 無効なトークン: '>>>'
次に、foo > 0
に変更しますが、パラメータ foo
を渡さないと、実行に失敗します。
2025/03/19 00:00:00 評価エラー: パラメータ 'foo' が見つかりません。
他のエラーは自分で確認できます。
結論
govaluate でサポートされている操作と型は限られていますが、それでもいくつかの興味深い機能を実装できます。 たとえば、ユーザーが独自の式を記述し、パラメータを設定して、サーバーに結果を計算させる Web サービスを作成できます。
Leapcell: 最高のサーバーレス Web ホスティング
最後に、Go サービスのデプロイに最適なプラットフォームをお勧めします: Leapcell
🚀 お気に入りの言語で構築
JavaScript、Python、Go、または Rust で簡単に開発。
🌍 無制限のプロジェクトを無料でデプロイ
使用量に対してのみ料金を支払います。リクエストも料金もありません。
⚡ 従量課金制、隠れたコストなし
アイドル料金はなく、シームレスなスケーラビリティのみ。
🔹 Twitter でフォローしてください: @LeapcellHQ