はじめに
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
の値を計算していますが、この式にはパラメータが必要ないので、Evaluate()
メソッドに nil
値を渡しています。もちろん、この例はあまり実用的ではありません。明らかに、コード内で直接 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言語では、アクセサを使って .
演算子で構造体のフィールドにアクセスします。渡されたパラメータの中に構造体型がある場合、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("ユーザー", result)
expr, _ = govaluate.NewEvaluableExpression("u.Age > 18")
result, _ = expr.Evaluate(parameters)
fmt.Println("年齢 > 18?", result)
}
上記のコードでは、User
構造体を定義し、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("ユーザー", 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