Go言語とGovaluateでルールエンジンを作る

[![Group157.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/e362dab5-ef71-4629-af6b-9d3eee5e1187.png)](http://leapcell.io/?lc_t=q_govaluate)



**[Leapcell: 最高のサーバーレスWebホスティング](http://leapcell.io/?lc_t=q_govaluate)**

### はじめに
2024年に、私はgovaluateを使ってルールエンジンを作成しました。その利点は、Go言語に動的言語の機能を持たせ、いくつかの計算操作を行い結果を得ることができる点にあります。これにより、対応するコードを書かずに、文字列を設定するだけで機能を実現できます。ルールエンジンの構築に非常に適しています。

### クイックスタート
まず、インストールします。
```bash
$ go get github.com/Knetic/govaluate
```

次に、使い方を説明します。
```go
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つのステップがあります。
1. `NewEvaluableExpression()` を呼び出して、式を式オブジェクトに変換します。
2. 式オブジェクトの `Evaluate` メソッドを呼び出し、パラメータを渡して、式の値を返します。

上記の例は簡単な計算を示しています。govaluateを使って `5 > 0` の値を計算していますが、この式にはパラメータが必要ないので、`Evaluate()` メソッドに `nil` 値を渡しています。もちろん、この例はあまり実用的ではありません。明らかに、コード内で直接 `5 > 0` を計算する方が便利です。しかし、場合によっては、計算する必要のある式のすべての情報を知らないことがあり、式の構造すら知らないこともあります。このような場合に、govaluateの役割が際立ちます。

### パラメータ
govaluateは、式の中でパラメータを使用することをサポートしています。式オブジェクトの `Evaluate()` メソッドを呼び出すときに、`map[string]interface{}` 型を通じてパラメータを渡して計算することができます。このうち、マップのキーはパラメータ名で、値はパラメータ値です。例えば：
```go
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つあります。
1. 名前を `[` と `]` で囲みます。例えば、`[leapcell_resp-time]`。
2. `\` を使って、その直後の文字をエスケープします。

例えば：
```go
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番目の式では `\\` を使用する必要があります。または、以下のようにすることもできます。
```go
`leapcell_resp\-time` < 100
```

### 一度「コンパイル」して複数回実行
パラメータ付きの式を使うことで、式を一度「コンパイル」して複数回実行することができます。コンパイルで返された式オブジェクトを使って、その `Evaluate()` メソッドを複数回呼び出すだけです。
```go
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)` です。関数がエラーを返す場合、この式の評価もエラーを返します。
```go
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も `.` を使ってその内部のフィールドにアクセスしたり、メソッドを呼び出したりすることをサポートしています。
```go
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()` メソッドを呼び出して取得します。
```go
// src/github.com/Knetic/govaluate/parameters.go
type Parameters interface {
  Get(name string) (interface{}, error)
}
```

例えば、`User` が `Parameter` インターフェースを実装するようにすることができます。
```go
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()` メソッドも呼び出しています。
```go
// 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` 型に変換し、その後それらの大小を比較します。
```go
func main() {
  expr, _ := govaluate.NewEvaluableExpression("'2025-03-02' > '2025-03-01 23:59:59'")
  result, _ := expr.Evaluate(nil)
  fmt.Println(result)
}
```

### エラーハンドリング
上記の例では、エラーハンドリングを意図的に無視しています。実際には、govaluateは式オブジェクトの作成と式の評価の両方の操作でエラーを発生させる可能性があります。式オブジェクトを生成するときに、式に構文エラーがある場合、エラーが返されます。式を評価するときに、渡されたパラメータが不正であったり、一部のパラメータが欠けていたり、構造体の存在しないフィールドにアクセスしようとしたりすると、エラーが報告されます。
```go
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)
}
```

式の文字列を順番に変更して、さまざまなエラーを検証することができます。まず、`>>>` とします。
```bash
2025/03/19 00:00:00 構文エラー:無効なトークン: '>>>'
```

次に、`foo > 0` に変更しますが、パラメータ `foo` を渡さないと、実行に失敗します。
```bash
2025/03/19 00:00:00 評価エラー:パラメータ 'foo' が見つかりません。
```

他のエラーは自分で検証することができます。

### まとめ
govaluateがサポートする演算と型は限られていますが、依然として面白い機能を実装することができます。例えば、ユーザーが自分で式を書き、パラメータを設定し、サーバーに結果を計算させることができるWebサービスを書くことができます。

# [Leapcell: 最高のサーバーレスWebホスティング](http://leapcell.io/?lc_t=q_govaluate)

最後に、Goサービスをデプロイするのに最適なプラットフォームをおすすめします：**[Leapcell](http://leapcell.io/?lc_t=q_govaluate)**

[![Group157.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/d6de3622-8ea6-44d1-9ace-df62a088eb58.png)](http://leapcell.io/?lc_t=q_govaluate)


### 🚀 好きな言語で構築
JavaScript、Python、Go、またはRustで簡単に開発できます。

### 🌍 無制限のプロジェクトを無料でデプロイ
使用した分だけ料金がかかります。リクエストがなければ、料金はかかりません。

### ⚡ 従量制課金、隠れた費用は一切ありません
アイドル料金はかからず、シームレスなスケーラビリティが保証されます。

[![Frame3-withpadding2x.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3967621/e96b5881-4a98-464b-8369-2d91cf241478.png)](http://leapcell.io/?lc_t=q_govaluate)


[📖 ドキュメントを探索する](https://docs.leapcell.io/)

🔹 Twitterでフォローしましょう：[@LeapcellHQ](https://x.com/LeapcellHQ)