SoFunction
Updated on 2025-03-05

golang rule engine gengine usage case

introduction

This article is receivedGolang interview classic explanationThe [Go Tool Library] B website’s new generation golang rules engine gengine reveals. The basic usage and the details of the author are basically mentioned. If you are concerned, you can check out the design documents and the author’s comparison.gopher_luaComparison

This article mainly contains several cases and usages of the author

Part1 Basic use

package main
import (
 "fmt"
 "/bilibili/gengine/builder"
 "/bilibili/gengine/context"
 "/bilibili/gengine/engine"
 "/sirupsen/logrus"
 "time"
)
func main() {
 TestMulti()
}
type User struct {
 Name string
 Age  int64
 Male bool
}
func (u *User) GetNum(i int64) int64 {
 return i
}
func (u *User) Print(s string) {
 (s)
}
func (u *User) Say() {
 ("hello world")
}
// Define rulesconst rule1 = `
rule "name test" "i can"  salience 0
begin
    if 7 == (7){
       = (89767) + 10000000
      ("6666")
    }else{
       = "yyyy"
    }
end
`
func TestMulti() {
 user := &User{
  Name: "Calo",
  Age:  0,
  Male: true,
 }
 dataContext := ()
 //Inject the initialized structure ("User", user)
 //init rule engine
 ruleBuilder := (dataContext)
 start1 := ()
 //Build rules err := (rule1) //string(bs)
 end1 := ()
 ("rules num:%d, load rules cost time:%d", len(), (start1).Milliseconds())
 if err != nil {
  ("err:%s ", err)
 } else {
  eng := ()
  start := ().UnixNano()
  //Execute the rules  err := (ruleBuilder, true)
  println()
  end := ().UnixNano()
  if err != nil {
   ("execute rule error: %v", err)
  }
  ("execute rule cost %d ns", end-start)
  ("=%d,Name=%s,Male=%t", , , )
 }
}

 INFO[0000] rules num:1, load rules cost time:5          
6666
10089767
INFO[0000] execute rule cost 97000 ns                   
INFO[0000] =10089767,Name=Calo,Male=true 

Part2 @name

Use @name in the rule body, which refers to the current rule name. When the rule is executed, @name will be parsed into a rule name string (name perception within the rule)

package main
import (
 "fmt"
 "/bilibili/gengine/builder"
 "/bilibili/gengine/context"
 "/bilibili/gengine/engine"
 "time"
)
func PrintName(name string) {
 (name)
}
/*
*
use '@name',you can get rule name in rule content
*/
const atNameRule = `
rule "Test rule name 1" "rule desc"
begin
  va = @name
  PrintName(va)
  PrintName(@name)
end
rule "rule name" "rule desc"
begin
  va = @name
  PrintName(va)
  PrintName(@name)
end
`
func exec() {
 dataContext := ()
 ("PrintName", PrintName)
 //init rule engine
 ruleBuilder := (dataContext)
 //resolve rules from string
 start1 := ().UnixNano()
 err := (atNameRule)
 end1 := ().UnixNano()
 println(("rules num:%d, load rules cost time:%d ns", len(), end1-start1))
 if err != nil {
  panic(err)
 }
 eng := ()
 start := ().UnixNano()
 // true: means when there are many rules, if one rule execute error,continue to execute rules after the occur error rule
 err = (ruleBuilder, true)
 end := ().UnixNano()
 if err != nil {
  panic(err)
 }
 println(("execute rule cost %d ns", end-start))
}
func main() {
 exec()
}

 rules num:2, load rules cost time:2820000 ns
Test rule name 1
Test rule name 1
rule name
rule name
execute rule cost 72000 ns

@name mainly gets the name of the rule

Part3 @id

The meaning of using @id in the rule body is that if the current rule name is a string that can be converted into an integer, then @id is the integer value of the rule name. If the rule name string cannot be converted into an integer, then the value of @id is 0. This is to facilitate users to use the rule name as an integer parameter

package main
import (
 "fmt"
 "/bilibili/gengine/builder"
 "/bilibili/gengine/context"
 "/bilibili/gengine/engine"
)
/*
*
use '@id',you can get rule name in rule content
*/
const atIDRule = `
rule "Test rule name 1" "rule desc" salience 10
begin
 println(@id)
end
rule " 100 " "rule desc" salience 20
begin
 x = @id
 println(x)
end
`
func TestAtId() {
 dataContext := ()
 ("println", )
 //init rule engine
 ruleBuilder := (dataContext)
 //resolve rules from string
 err := (atIDRule)
 if err != nil {
  panic(err)
 }
 eng := ()
 err = (ruleBuilder, false)
 if err != nil {
  panic(err)
 }
}
type Data struct {
 M map[string]string
}
func (d *Data) exe() {
 println("hhhh")
}
func main() {
 TestAtId()
}
100
0

Part4 @desc syntax

Get the description information of the current rule in the rule body, the type is a string

package main
import (
 "fmt"
 "/bilibili/gengine/builder"
 "/bilibili/gengine/context"
 "/bilibili/gengine/engine"
 "time"
)
/*
*
use '@desc',you can get rule description in rule content
*/
const atDescRule = `
rule "rule name 1" "I'm a test description information 1" salience 100
begin
  desc = @desc
  Print(desc)
  Print(@name + " : " + @desc)
end
rule "rule name 2" //"I am a description, desc" sales 10begin
  desc = @desc
  Print(desc)
  Print(@name + " : " + @desc)
end
`
func main() {
 dataContext := ()
 // ("Print", PrintName)
 ("Print", )
 //init rule engine
 ruleBuilder := (dataContext)
 //resolve rules from string
 start1 := ().UnixNano()
 err := (atDescRule)
 end1 := ().UnixNano()
 println(("rules num:%d, load rules cost time:%d ns", len(), end1-start1))
 if err != nil {
  panic(err)
 }
 eng := ()
 // true: means when there are many rules, if one rule execute error,continue to execute rules after the occur error rule
 err = (ruleBuilder, true)
 if err != nil {
  panic(err)
 }
}

 rules num:2, load rules cost time:3172000 ns
I'm a test description information 1
rule name 1: I am a test description information 1

rule name 2 : 

Note that if there is // is a comment, it cannot be parsed.

Part5 sal

Get the priority information of the current rule in the body of the rule, type int64

package main
import (
 "fmt"
 "/bilibili/gengine/builder"
 "/bilibili/gengine/context"
 "/bilibili/gengine/engine"
 "time"
)
func main() {
 dataContext := ()
 ("println", )
 //init rule engine
 ruleBuilder := (dataContext)
 err := (`
rule "1" salience 10
begin
 println(@sal)
end
rule "2" 
begin
 println(@sal)
end
`)
 if err != nil {
  panic(err)
 }
 eng := ()
 start := ().UnixNano()
 // true: means when there are many rules, if one rule execute error,continue to execute rules after the occur error rule
 err = (ruleBuilder, true)
 end := ().UnixNano()
 if err != nil {
  panic(err)
 }
 println(("execute rule cost %d ns", end-start))
}
10
0
execute rule cost 120000 ns

Part6 Comments

Supports single-line comments in rules, comments start with double slashes (//)

Part7 Custom variables

User-defined variables do not need to declare types. Variables defined in the rule are only visible to the current rule and are not visible to other rules (local variables). Use the (variable) data injected by dataContext, and all rules loaded into the gengine are visible to all rules (global variables)

Part8 Line number prompt when reporting an error

The syntax supported by gengine is a complete DSL syntax (can also be regarded as a complete language). When a gengine rule is executed, gengine will point out which line the specific error is on. Try to help users have a silky experience in every process detail of using gengine.

package main
import (
 "fmt"
 "/bilibili/gengine/builder"
 "/bilibili/gengine/context"
 "/bilibili/gengine/engine"
)
var lineNumberRules = `
rule "line_number"  "when execute error,gengine will give out error"
begin
//Println("golang", "hello", "world" )
//Cancel the slash comment and test different error reports in turn//if Println("golang", "hello") == 100 {
//  Println("golang", "hello")
//}
()
end
`
type MyStruct struct {
}
func (m *MyStruct) XX(s string) {
 println("XX")
}
func Println(s1, s2 string) bool {
 println(s1, s2)
 return false
}
func main() {
 dataContext := ()
 //Inject custom functions ("Println", Println)
 ms := &MyStruct{}
 ("ms", ms)
 ruleBuilder := (dataContext)
 e1 := (lineNumberRules)
 if e1 != nil {
  panic(e1)
 }
 eng := ()
 // true: means when there are many rules, if one rule execute error,continue to execute rules after the occur error rule
 e2 := (ruleBuilder, true)
 if e2 != nil {
  println(("%+v", e2))
 }
}

 [rule: "line_number" executed, error:
 line 12, column 0, code: (), NOT FOUND Function: X ]

Part9 Multi-level Call Support

The form of third-level calls has been supported now, but calls above level three do not support other details:

1. When the syntax is C, or a=C, then C can be a specific value, variable, function or method, structure (pointer), map, slice, array, etc., for example, a=100, a = Mp["hello"], a = x, a = getMessage(p1,p2..) etc.

2. When the syntax is, or a=, then A must be a structure (pointer), C is the same as above, such as a = ["hello"], a = A.Field1, a = (p1,p2..) etc.

3. When the syntax is, or a=, then A and B must be structures (pointers), C is the same as above, such as a = ["hello"], a =.Field1, a= (p1, p2..) etc.

Then, if the syntax is right, ["hello"].C is illegal

package main
import (
 "fmt"
 "/bilibili/gengine/builder"
 "/bilibili/gengine/context"
 "/bilibili/gengine/engine"
)
type B struct {
 Name string
 Mp   map[string]string
 Ar   [5]string
 Sl   []int
}
func (b *B) Meth(s string) {
 println("--->", s)
}
func main() {
 type A struct {
  N  string
  Ma map[string]string
  B  *B
 }
 rule := `
rule "three level call"
begin
 conc{
  = "xiaoMing"
 ["hello"] = "world"
 [1] = "Calo"
 [2] = 3
 x = [0]
 ([1])
  = "kakaka"
 ["a"] = "b"
 }
 println(, ["hello"], [1], [2], x, , ["a"])
 if [0] == 0 {
  println(true)
 }
end
`
 b := B{
  Name: "",
  Mp:   make(map[string]string),
  Ar:   [5]string{},
  Sl:   make([]int, 6),
 }
 pA := &A{
  N:  "",
  Ma: make(map[string]string),
  B:  &b,
 }
 dataContext := ()
 ("println", )
 ("A", pA)
 ruleBuilder := (dataContext)
 e := (rule)
 if e != nil {
  panic(e)
 }
 gengine := ()
 e = (ruleBuilder, true)
 if e != nil {
  panic(e)
 }
 println(, ["hello"], [1], [2])
}

 ---> Calo
xiaoMing world Calo 3 0 kakaka b
true
xiaoMing world Calo 3

The above is the detailed content of the golang rule engine gengine usage case. For more information about golang rule engine gengine, please follow my other related articles!