Preventing Goroutine Leaks: Best Practices and Tips for Go Developers

Preventing Goroutine Leaks: Best Practices and Tips for Go Developers

Concurrency Design Pattern Part - 1

This is the first blog in the series of Concurrency Design Patterns in Golang. I'll be adding more blogs with many complicated design patterns. If you want to be notified do subscribe to my Newsletter

Before starting with the design pattern, here are some prerequisites.

  • It would be best if you were comfortable with Golang's syntax.

Let's brush up on a few concepts before discussing the design pattern. (If you know all of these feel free to skip to the design pattern)

Goroutines

To put it simply, a goroutine is a part of code that run concurrently alongside other code. We won't go into much theory (we can leave that for some other blog).

To declare a goroutine you just have to use the go prefix while invoking a function.

Here's a simple example:

func main(){

go func(){
    for i:= 0 ;i<5;i++{
        fmt.Print(i + " ")
    }
}()

go func(){
    for i:= 5 ;i<10;i++{
        fmt.Println(i + " ")
    }
}()
}

The output of this code snippet will be something like this:

0 1 5 2 6 7 3 8 9 4
# It does not have to be exactly same.

Channels

Channels are one of the most essential building blocks of our concurrency design patterns. To put it very simply

Channels are just queues that can be used to send data from one goroutine to another.

Here are a few important facts about channels:

  • Channels can be either buffered (can hold one or more values) or unbuffered (cannot hold any values)

  • Channels are blocking.

    • If you are pushing an object to a filled channel then the program won't proceed further until another object is popped from the channel

    • If you are popping the element from an empty channel then the program won't proceed further until an object is pushed to the channel.

// This is a read channel. You can only read values
var readChannel <-chan interface{} 

// This is a write channel. You can only write values to this channel
var writeChannel chan<- interface{}

//You can read as well as write to this channel
var channel chan interface{}

channel1 <- val // we are pushing a value to a channel
val <- channel2 // we are reading from a channel

Now let's jump to the fun part

What is a goroutine leak?

We use a goroutine to perform some operations, and after some time send us the result and terminate. But what happens if it doesn't terminate? well, this is what we call a goroutine leak.

When a goroutine keeps on consuming system resources throughout the runtime of the program we call it a goroutine leak.

Even though goroutines are lightweight, if we invoke a lot of goroutines then we will be wasting a lot of resources.

Before we discuss, how we are gonna prevent goroutine leaks, let's check out an example:

func test() <-chan int {
    outStream := make(chan int)
    go func(outStream chan<- int) {
        for i := 0; i < 10; i-- {
            outStream <- i
        }
    }(outStream)
    return outStream
}

func main() {
    inStream := test()
    for i := 0; i < 10; i++ {
        val := <-inStream
        fmt.Println(val)
        time.Sleep(1 * time.Second)
    }
}

Here we have a test function that returns a read channel (we can only read values from this channel). It then invokes a goroutine and passes the channel as a write channel (we can only write to this channel). We are running a for loop and writing values to the channel. But here's the interesting part, the for loop is infinite!!. We have done this to mimic the case where our goroutine does not terminate and keeps sending value.

When we run the code, we get the following output.

We stopped the loop after 10 iterations but it can go on forever.

Now that we have seen a goroutine leak in action, let's discuss the solution.

  • To stop the goroutine from executing forever, the parent needs to send it some signal. By parent, we mean the function that invoked the goroutine.

  • When the child receives the signal, it stops its operation. Since goroutines can only communicate via channels, the parent will send a channel to its child goroutine.

  • Once the parent wants to terminate the child it simply closes the channel. This signal is received by the child and it also terminates its action.

Let's look at the code implementation to understand it better

func test(terminate <-chan bool) <-chan int {
    outStream := make(chan int)
    go func(terminate <-chan bool, outStream chan<- int) {
        defer close(outStream) // Channel is closed when the function is finishes executing
        for i := 0; i < 10; i-- {
            select {
            case <-terminate:
                fmt.Println("Child Terminating")
                return
            case outStream <- i:
            }
        }
    }(terminate, outStream)
    return outStream
}

func main() {
    terminate := make(chan bool)

    inStream := test(terminate)
    for i := 0; i < 10; i++ {
        val, ok := <-inStream

       // If !ok, then it means channel is closed and we won't receive new value
        if !ok {
            fmt.Println("Channel closed")
            break
        }

        fmt.Println(val)
        time.Sleep(1 * time.Second)
        if i == 5 {
            close(terminate)
        }
    }
}

Okay, let's discuss how this code works

  • We make a new channel called 'terminate', and pass it as a read-only channel to the child.

  • Inside the infinite for-loop, we use a select statement. (A select statement is like a switch statement, it executes blocks of codes when their corresponding channels send or receive data).

  • Now, when the channel is closed by the parent, a closing signal is sent to the child. It then executes its corresponding code block i.e., a return statement. which breaks the infinite loop.

  • Now the goroutine terminates, and closes the 'outstream' channel and thus terminating the child.

Now if we execute the code, it looks something like this:

Thanks for reading till the end. If you liked my blog then do subscribe to my newsletter for more awesome content.


Cover picture credit: https://www.storj.io/blog/finding-goroutine-leaks-in-tests