Skip to main content

Command Palette

Search for a command to run...

Golang - Interpreter Pattern

Updated
4 min read
Golang - Interpreter Pattern
M

Senior Freelancer & Technical Lead

Working as a Golang developer since 2020. Working as a mobile developer since 2013.

Focussed on architecture, testability and clean code. Open minded & product driven. Based in Rhede, available world-wide

Introduction

The Interpreter pattern is a behavioral pattern from the Gang of Four (GoF) design patterns that provides a way to evaluate and interpret a language grammar or expression. The pattern defines a language and its grammar, and then provides an interpreter that can interpret the language and execute the grammar. The Interpreter pattern is useful for implementing domain-specific languages and for parsing and interpreting user input.

Example

In Golang, the interpreter pattern can be implemented easily using the combination of interfaces, structs, and functions. Here is a more practical example of how to implement the Interpreter pattern in Golang.

Suppose we have an e-commerce website and we want to provide users with a way to filter products based on certain criteria such as price range, brand, color, etc. We can define a domain-specific language (DSL) to represent these filters and use the Interpreter pattern to parse and execute the DSL.

Let's define our DSL:

  • price represents the price filter

  • brand represents the brand filter

  • color represents the color filter

Each filter has a specific syntax:

  • price:range(min,max) where min and max are integers representing the minimum and maximum prices respectively

  • brand:brand_name where brand_name is a string representing the name of the brand

  • color:color_name where color_name is a string representing the name of the color

We can define our Interpreter interface and implement it for each filter type:

type Interpreter interface {
    Interpret(context *Context) []Product
}

type PriceFilter struct {
    min int
    max int
}

func (pf *PriceFilter) Interpret(context *Context) []Product {
    products := context.Products
    filtered := []Product{}
    for _, p := range products {
        if p.Price >= pf.min && p.Price <= pf.max {
            filtered = append(filtered, p)
        }
    }
    return filtered
}

type BrandFilter struct {
    brand string
}

func (bf *BrandFilter) Interpret(context *Context) []Product {
    products := context.Products
    filtered := []Product{}
    for _, p := range products {
        if p.Brand == bf.brand {
            filtered = append(filtered, p)
        }
    }
    return filtered
}

type ColorFilter struct {
    color string
}

func (cf *ColorFilter) Interpret(context *Context) []Product {
    products := context.Products
    filtered := []Product{}
    for _, p := range products {
        if p.Color == cf.color {
            filtered = append(filtered, p)
        }
    }
    return filtered
}

Next, we define a Context struct that contains the list of products and a Parser struct that can parse the DSL and build the filter tree:

type Product struct {
    Name  string
    Price int
    Brand string
    Color string
}

type Context struct {
    Products []Product
}

type Parser struct {
    stack []Interpreter
}

func (p *Parser) Parse(expression string) {
    tokens := strings.Split(expression, " ")
    for _, token := range tokens {
        parts := strings.Split(token, ":")
        switch parts[0] {
        case "price":
            args := strings.Trim(parts[1], "range()")
            prices := strings.Split(args, ",")
            min, _ := strconv.Atoi(prices[0])
            max, _ := strconv.Atoi(prices[1])
            filter := &PriceFilter{min: min, max: max}
            p.stack = append(p.stack, filter)
        case "brand":
            brand := parts[1]
            filter := &BrandFilter{brand: brand}
            p.stack = append(p.stack, filter)
        case "color":
            color := parts[1]
            filter := &ColorFilter{color: color}
            p.stack = append(p.stack, filter)
        }
    }
}

func (p *Parser) Result(context *Context) []Product {
    for _, i := range p.stack {
        context.Products = i.Interpret(context)
    }
    return context.Products
}

Finally, we can use the Parser to filter our list of products:

func main() {
    products := []Product{
        {Name: "Product 1", Price: 100, Brand: "Brand 1", Color: "Red"},
        {Name: "Product 2", Price: 50, Brand: "Brand 2", Color: "Blue"},
        {Name: "Product 3", Price: 200, Brand: "Brand 1", Color: "Green"},
    }
    context := &Context{Products: products}

    parser := &Parser{}
    parser.Parse("price:range(50,150) brand:Brand 1 color:Green")

    filtered := parser.Result(context)
    fmt.Println(filtered)
}

In this example, we define a DSL to filter products based on price range, brand, and color. We use the Interpreter pattern to parse and execute the DSL, resulting in a list of filtered products.

Conclusion

In conclusion, the Interpreter pattern is a powerful pattern that provides a way to evaluate and interpret a language grammar or expression. In Golang, it can be easily implemented using the combination of interfaces, structs, and functions. It can be used to implement domain-specific languages, parse and interpret user input, and filter data. By using the Interpreter pattern, we can create more powerful and expressive applications that can understand and execute complex user input and commands.


References

Golang Patterns

Part 9 of 19

Welcome to the Golang Patterns tutorial series! In this series, we will explore the various software patterns that can be applied in Golang to create flexible, scalable, and maintainable software.

Up next

Golang - Facade Pattern

It's all just a facade

More from this blog

Matthias Bruns

69 posts