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.
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,
}
}