0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

Group157.png

Leapcell: 最高のサーバーレスWebホスティング

はじめに

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つのステップがあります。

  1. NewEvaluableExpression() を呼び出して、式を式オブジェクトに変換します。
  2. 式オブジェクトの 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つあります。

  1. 名前を [] で囲みます。例えば、[leapcell_resp-time]
  2. \ を使って、その直後の文字をエスケープします。

例えば:

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 = 1b = 2 を渡し、結果は 3 です。2回目の実行時には、パラメータ a = 10b = 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)
}

例えば、UserParameter インターフェースを実装するようにすることができます。

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))
}

式を評価するときには、ParameterGet() メソッドを呼び出して未知のパラメータを取得する必要があります。上記の例では、FullName を直接使って u.Get() メソッドを呼び出し、フルネームを返すことができます。

サポートされる演算と型

govaluateがサポートする演算と型は、Go言語とは異なります。一方で、govaluateの型と演算はGoほど豊富ではありません。他方で、govaluateはいくつかの演算を拡張しています。

算術、比較、論理演算

  • + - / * & | ^ ** % >> <<:加算、減算、乗算、除算、ビットAND、ビットOR、XOR、べき乗、剰余、左シフト、右シフト。
  • > >= < <= == != =~ !~=~ は正規表現のマッチング、!~ は正規表現の非マッチングです。
  • || &&:論理ORと論理AND。

定数

  • 数値定数。govaluateでは、数値はすべて64ビット浮動小数点数として扱われます。
  • 文字列定数。なお、govaluateでは、文字列はシングルクォート ' で囲みます。
  • 日付と時刻の定数。形式は文字列と同じです。govaluateは自動的に文字列が日付かどうかを解析しようとし、RFC3339やISO8601などの限られた形式のみをサポートします。
  • ブール定数:truefalse

その他

  • 括弧を使って計算の優先順位を変更することができます。
  • 配列は () で定義され、各要素は , で区切られます。任意の要素型をサポートします。例えば、(1, 2, 'foo')。実際には、govaluateでは配列は []interface{} で表されます。
  • 三項演算子:? :

以下のコードでは、govaluateは最初に 2025-03-022025-03-01 23:59:59time.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

Group157.png

🚀 好きな言語で構築

JavaScript、Python、Go、またはRustで簡単に開発できます。

🌍 無制限のプロジェクトを無料でデプロイ

使用した分だけ料金がかかります。リクエストがなければ、料金はかかりません。

⚡ 従量制課金、隠れた費用は一切ありません

アイドル料金はかからず、シームレスなスケーラビリティが保証されます。

Frame3-withpadding2x.png

📖 ドキュメントを探索する

🔹 Twitterでフォローしましょう:@LeapcellHQ

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?