Goroutine leaks

The worst nightmare while using Goroutines

Goroutines

Goroutines are cheap to create. However, goroutines have a finite cost in terms of memory footprint, so exploiting them is not a good idea.

Every time you use a goroutine, you must know how and when the goroutine will exit.

Goroutine Leaks

Goroutine leak is a type of memory leak, wherein you accidentally start a goroutine which will never terminate, hence, occupying memory it has reserved.

Examples of Goroutine Leaks

1. Channels

Channels, when handled incorrectly can cause goroutine leaks.

Here is a basic example where a query result is sent to a channel, but is not received properly:

func main() {
	for i := 0; i < 5; i++ {
		fetchAll()
		fmt.Printf("Number of goroutines: %d\n", runtime.NumGoroutine())
	}
}

// just a random query fetching logic
// (for demonstration purposes only)
func fetchAll() int {
	ch := make(chan int)

	// sent multiple times
	for i := 0; i < 2; i++ {
		go func() {
			ch <- query()
		}()
	}

	// received only once
	return <-ch
}

// a basic demo of a query
func query() int {
	n := rand.Intn(200)
	time.Sleep(time.Duration(n) * time.Millisecond)
	return n
}

Output:

Number of goroutines: 2
Number of goroutines: 3
Number of goroutines: 4
Number of goroutines: 5
Number of goroutines: 6

Oops! As expected, we can see that the query result is being sent to the channel twice, but is received only once. Hence, a goroutine leak is introduced. With each call to fetchAll(), a goroutine starts leaking.

2. Slow goroutines

This is really common while dealing with web requests. If you’re dealing with requests on a remote URL, the response times can be high sometimes.

Remember that http.Client doesn’t have a timeout. If the request is done inside a goroutine, and the response time is high, goroutine leaks might occur.

Here is an example to demonstrate this:

package main

import (
	"fmt"
	"net/http"
	"runtime"
	"time"
)

func main() {
	for {
		go func() {
			// url with 5s delay
			_, err := http.Get("https://httpstat.us/200?sleep=5000")
			if err != nil {
				fmt.Printf("%v\n", err)
			}
		}()

		time.Sleep(time.Second * 1)
		fmt.Println("Number of goroutines:", runtime.NumGoroutine())
	}
}

Output:

Number of goroutines: 3
Number of goroutines: 4
Number of goroutines: 5
Number of goroutines: 6

How to detect Goroutine leaks?

The easiest option is to use runtime.NumGoRoutine in your tests. But there is a better option.

The Go team at Uber (awesome team; highly active on Github), created goleak, which monitors for goroutine leaks in the currently tested piece of code.

Here is an example, where I use goleak to detect goroutine leaks:

// main.go

import (
	"time"
)

func FunctionWithGoRoutineLeak() error {
	go func() {
		time.Sleep(time.Minute)
	}()

	return nil
}

The test:

// main_test.go

import (
	"log"
	"testing"

	"go.uber.org/goleak"
)

func TestLeak(t *testing.T) {
	defer goleak.VerifyNone(t)

	if err := FunctionWithGoRoutineLeak(); err != nil {
		log.Println("Goroutine Leak!")
	}
}

On testing with go test, here is our result:

❯ go test
--- FAIL: TestLeak (0.45s)
    leaks.go:78: found unexpected goroutines:
        [Goroutine 7 in state sleep, with time.Sleep on top of the stack:
        goroutine 7 [sleep]:
        time.Sleep(0xdf8475800)
                /usr/local/go/src/runtime/time.go:193 +0xd2
        example%2ecom.FunctionWithGoRoutineLeak.func1()
                /home/aadhav/go-vault/main.go:9 +0x30
        created by example%2ecom.FunctionWithGoRoutineLeak
                /home/aadhav/go-vault/main.go:8 +0x35
        ]
FAIL
exit status 1
FAIL    example.com     0.450s

Woohoo! The leaking goroutine has been detected successfully!

The error message contains information about the leaked goroutine with its state.

This might not be the most robust solution out here, but it can help detect leaks directly from tests. If you love to write tests, this might be the best solution. If not, you can always use pprof!