动手实现基于Go语言的规则引擎

293 阅读3分钟

今天我们来实现一个基于Go语言的规则引擎,实现词法分析、语法分析和抽象语法树的执行功能。

词法分析

词法分析的主要任务是将规则表达式解析为一系列的token,也就是关键字。在本例中,我们可以定义一个token结构体,包含token类型和token值两个字段。

go复制代码
type Token struct {
    tokenType   string // token类型
    tokenValue  string // token值
}

然后,在词法分析器函数中,我们可以使用正则表达式来匹配token:

go复制代码
func Lexer(expression string) []Token {
    var tokens []Token
    
    pattern := [...]struct{
        regex       *regexp.Regexp
        tokenType   string
    }{
        { regexp.MustCompile(`(`), "LEFT_BRACKET" },
        { regexp.MustCompile(`)`), "RIGHT_BRACKET" },
        { regexp.MustCompile(`AND|OR`), "LOGICAL_OPERATOR" },
        { regexp.MustCompile(`[0-9]+`), "NUMBER" },
        { regexp.MustCompile(`[a-zA-Z]+`), "STRING" },
    }
    
    for len(expression) > 0 {
        // 匹配符合pattern规则的token
        for _, rule := range pattern {
            if match := rule.regex.FindStringIndex(expression); match != nil {
                tokens = append(tokens, Token{rule.tokenType, expression[:match[1]]})
                expression = expression[match[1]:]
                break
            }
        }
    }
    
    return tokens
}

在这个例子中,我们匹配了括号、逻辑运算符、数字和字符串等不同类型的token,并将它们保存在tokens数组中返回。

语法分析

在词法分析之后,我们需要将tokens数组解析为语法树,也就是抽象语法树(AST)。在本例中,我们可以定义以下AST节点类型:

go复制代码
type AstNode struct {
    nodeType                string
    left, right, parent     *AstNode 
    value                   string
}

在定义好AST节点类型后,我们可以使用LL(1)语法分析器来生成AST。在本例中,我们的语法规则如下:

复制代码
expression : logic_expression

logic_expression : comparision_expression (LOGICAL_OPERATOR comparision_expression)*

comparision_expression : (STRING|NUMBER) COMPARISION_OPERATOR (STRING|NUMBER)

以下是实现LL(1)语法分析的代码:

go复制代码
func Parser(tokens []Token) *AstNode {
    var (
        currentNode = &AstNode{
            nodeType: "ROOT",
        }
        tokenIndex = 0
        curToken = tokens[tokenIndex]
    )
    
    // 比较函数
    defComparator := func(tokenType string) bool {
        return curToken.tokenType == tokenType
    }
    
    // 表达式
    parseExpression := func() *AstNode {
        return parseLogicalExpression()
    }
    
    // 逻辑表达式
    parseLogicalExpression := func() *AstNode {
        node := parseComparisonExpression()
        
        for defComparator("LOGICAL_OPERATOR") {
            operator := curToken
            tokenIndex++
            right := parseComparisonExpression()
            node = &AstNode{
                nodeType: "LOGICAL_EXPRESSION",
                left: node,
                right: right,
                value: operator.tokenValue,
            }
        }
        
        return node
    }
    
    // 比较表达式
    parseComparisonExpression := func() *AstNode {
        left := &AstNode{nodeType: "COMPARISION_EXPRESSION", value: ""}
        right := &AstNode{nodeType: "COMPARISION_EXPRESSION", value: ""}
        
        if defComparator("STRING") || defComparator("NUMBER") {
            left = &AstNode{
                nodeType: "COMPARISION_EXPRESSION",
                value: curToken.tokenValue,
            }
            tokenIndex++
        } else {
            panic(fmt.Sprintf("Unexpected token type: %s", curToken.tokenType))
        }
        
        if defComparator("COMPARISION_OPERATOR") {
            operator := curToken
            tokenIndex++
            
            if defComparator("STRING") || defComparator("NUMBER") {
                right = &AstNode{
                    nodeType: "COMPARISION_EXPRESSION",
                    value: curToken.tokenValue,
                }
                tokenIndex++
                
                return &AstNode{
                    nodeType: "COMPARISION_EXPRESSION",
                    value: operator.tokenValue,
                    left: left,
                    right: right,
                }
            } else {
                panic(fmt.Sprintf("Unexpected token type: %s", curToken.tokenType))
            }
        } else {
            panic("Missing comparision operator")
        }
    }
    
    currentNode.left = parseExpression()
    
    return currentNode
}

在以上代码中,我们先定义了一个当前节点currentNode,并让它指向根节点。然后,在解析tokens数组的过程中,我们逐条检查每一个token,并且根据语法规则生成AST节点。

抽象语法树执行

最后一步是将抽象语法树进行执行,根据抽象语法树上的节点信息,执行规则引擎中定义的规则操作。我们可以为不同类型的AST节点定义不同的执行操作,以下是一个示例:

go复制代码
type Executor struct {}

func (e *Executor) execute(node *AstNode, context map[string]interface{}) bool {
    switch node.nodeType {
    case "ROOT":
        return e.execute(node.left, context)
    case "LOGICAL_EXPRESSION":
        switch node.value {
        case "AND":
            return e.execute(node.left, context) && e.execute(node.right, context)
        case "OR":
            return e.execute(node.left, context) || e.execute(node.right, context)
        }
    case "COMPARISION_EXPRESSION":
        left := node.left.value
        right := node.right.value
        
        switch node.value {
        case "gt":
            return left > right
        case "gte":
            return left >= right
        case "lt":
            return left < right
        case "lte":
            return left <= right
        case "eq":
            return left == right
        case "neq":
            return left != right
        }
    }
    
    return false
}

在以上代码中,我们使用了一个Executor结构体来定义不同类型的节点对应的执行函数。例如,当AST节点类型为LOGICAL_EXPRESSION时,我们会根据节点的value值决定使用AND或OR运算符,然后递归执行节点的左右孩子,并根据逻辑运算结果返回结果值。在其他节点类型下执行的操作类似。

为了让规则引擎更加通用,我们还可以通过传入一个context参数来使规则引擎的执行更加灵活。context是一个字符串和接口类型的映射,可以在执行规则时提供上下文环境信息。

实现完以上三个部分后,我们就可以使用一个基于规则表达式的规则引擎了。