Efficiency with Go channels



I've been building a Facebook bot recently using Golang, the bot checks the incoming message text for a certain set of strings. These are called 'rules' within the bot. Nothing fancy, just simple string searches, and checks against that users state etc.

However, after 8 or 9 different rules built up, being executed sequentially, I noticed something, probably imperceptible to the user. But the further a user got through each rule, the more rules that were being checked each request, the longer each request would take.

I couldn't keep a record of which rules had been previously validated because a user could hit the same rule multiple times. So I had to check each rule, each request.

So after some refactoring, I decided this might be a good use case for Go's incredibly versatile channels.

So here's the older, slower approach... (obviously this isn't the real code, but closely models the same logic used).

package main

import (  
    "fmt"
    "log"
    "time"
)

func timeTrack(start time.Time, name string) {  
    elapsed := time.Since(start)
    log.Printf("%s took %s", name, elapsed)
}

func main() {

    defer timeTrack(time.Now(), "Rules")

    ok := ruleOne()

    if ok {
        fmt.Println("Done!")
    }

    ok = ruleTwo()

    if ok {
        fmt.Println("Done!")
    }

    ok = ruleThree()

    if ok {
        fmt.Println("Done!")
    }

    ok = ruleFour()

    if ok {
        fmt.Println("Done!")
    }

    ok = ruleFive()

    if ok {
        fmt.Println("Done!")
    }
    ok = ruleSix()

    if ok {
        fmt.Println("Done!")
    }
}

func ruleOne() bool {  
    time.Sleep(1000)
    return false
}

func ruleTwo() bool {  
    time.Sleep(1000)
    return false
}

func ruleThree() bool {  
    time.Sleep(1000)
    return false
}

func ruleFour() bool {  
    time.Sleep(1000)
    return false
}

func ruleFive() bool {  
    time.Sleep(1000)
    return false
}

func ruleSix() bool {  
    time.Sleep(1000)
    return true
}

This works, but as you can see, it takes a period of time to check each rule, and the further through those rules you get, the longer it takes to complete the request.

So here's the refactored code using channels...

package main

import (  
    "fmt"
    "log"
    "time"
)

func timeTrack(start time.Time, name string) {  
    elapsed := time.Since(start)
    log.Printf("%s took %s", name, elapsed)
}

func main() {

    defer timeTrack(time.Now(), "Rules")

    finished := make(chan bool)

    go func() {
        ok := ruleOne()

        if ok {
            finished <- true
            return
        }
    }()

    go func() {
        ok := ruleTwo()

        if ok {
            finished <- true
            return
        }
    }()

    go func() {
        ok := ruleThree()

        if ok {
            finished <- true
            return
        }
    }()

    go func() {
        ok := ruleFour()

        if ok {
            finished <- true
            return
        }
    }()

    go func() {
        ok := ruleFive()

        if ok {
            finished <- true
            return
        }
    }()

    go func() {
        ok := ruleSix()

        if ok {
            finished <- true
            return
        }
    }()

    for {
        select {
        case isFinished := <-finished:
            fmt.Println("Done: ", isFinished)
            return
        }
    }
}

func ruleOne() bool {  
    time.Sleep(1000)
    return false
}

func ruleTwo() bool {  
    time.Sleep(1000)
    return false
}

func ruleThree() bool {  
    time.Sleep(1000)
    return false
}

func ruleFour() bool {  
    time.Sleep(1000)
    return false
}

func ruleFive() bool {  
    time.Sleep(1000)
    return false
}

func ruleSix() bool {  
    time.Sleep(1000)
    return true
}

As you can see here, we create a new channel, which takes a boolean. Then we spin off a new go routine for each rule, and if the rule matches, a boolean of 'true' is passed onto that channel, and the request is completed.

It shouldn't come as much of as a shock that the second approach using channels is faster, and it's not a particularly interesting use case for channels. But admittedly I still get excited, coming from a PHP and Javascript background when I come across these opportunities to use channels!

So let's get some stats!

2016/11/27 22:41:01 Approach one took 183.253µs  

And...

2016/11/27 22:41:05 Approach two took 68.152µs  

If you are using Ad Blocker, that's cool. But please consider donating a small amount for upkeep and... burritos!

https://monzo.me/ewanvalentine