Add a timeout to any function in Go

Channels are a truly wonderful tool, and they can be leveraged for a lot of use cases. One of these use cases is timeouts.

In this example, we’ll assume that we want to fetch something and return that something as a result, but we want to return an error if getting that something takes longer than 5 seconds.

The content of the data you’re trying to fetch is irrelevant for this example, but for the sake of completeness, this is what we’ll start with:

type Something struct {
    Id   int64
    Data []byte
}

func GetSomething(id int64) (*Something, error) {
    return getSomethingFromSomewhere(id)
}

The easiest way to add a timeout to any operation is to move the function itself one layer down by renaming it and having another function - with the exact same signature as your original function - call that function while also handling the timeout.

In English, this means:

1. Rename GetSomething to doGetSomething

func doGetSomething(id int64) (*Something, error) {
    return getSomethingFromSomewhere(id)
}

2. Create an empty function with the same original signature (GetSomething(id int64) (*Something, error))

func GetSomething(id int64) (*Something, error) {

}

3. Have your new function (GetSomething) call your original renamed function (doGetSomething)

Because we’re going to use a channel later and doGetSomething returns multiple values, we need to create a struct with all of these return values so that we can return the result into a single channel.

type getSomethingResult struct {
    Something *Something
    Error     error
}

As a result, we need to change the signature of the original function (doGetSomething). This is fine, because this function is no longer directly called by anything other than the function you just created with support for timeouts.

func doGetSomething(id int64) getSomethingResult {
    something, err := getSomethingFromSomewhere(id)
    return getSomethingResult{
        Something: something,
        Error:     err,
    }
}

4. Call your old function from your new function

In order to add support for timeouts, we’ll use a channel to pass the output of the original function through as well as a goroutine which will contain the call to the original function.

The goroutine will then send the result through the channel:

func GetSomething(id int64) (*Something, error) {
    result := make(chan getSomethingResult, 1)
    go func() {
        result <- doGetSomething(id)
    }()
}

5. Add support for a timeout

In order to add support for timeouts, we’ll use select with a case using <-time.After(5 * time.Second) as a value, which translates to “After 5 seconds, send a value to the channel”. In this case, select is the one awaiting the value.

func GetSomething(id int64) (*Something, error) {
    result := make(chan getSomethingResult, 1)
    go func() {
        result <- doGetSomething(id)
    }()
    select {
    case <-time.After(5 * time.Second):
        return nil, errors.New("timed out")
    case result := <-result:
        return result.Something, result.Error
    }
}

What the code above really does is wait for both the timeout and the result, but return whichever comes first.

If you’d like to test whether it works or not, you can add a time.Sleep(6 * time.Second) at the beginning of the goroutine function, which should cause the timeout of 5 seconds to be triggered first.

Final result

type Something struct {
    Id   int64
    Data []byte
}

type getSomethingResult struct {
    Something *Something
    Error     error
}

func GetSomething(id int64) (*Something, error) {
    result := make(chan getSomethingResult, 1)
    go func() {
        result <- doGetSomething(id)
    }()
    select {
    case <-time.After(5 * time.Second):
        return nil, errors.New("timed out")
    case result := <-result:
        return result.Something, result.Error
    }
}

func doGetSomething(id int64) getSomethingResult {
    something, err := getSomethingFromSomewhere(id)
    return getSomethingResult{
        Something: something,
        Error:     err,
    }
}