Wrap a slow function with a timeout using a channel and select
unpredictableFunc() int64 runs for an unbounded time (it does a network call inside) and you cannot change its body. Write predictableFunc that returns its result if it finishes within a deadline, or an error if the deadline passes first.
Constraints:
- Take a
context.Context; if it has no deadline, apply a default timeout. - Do not leak the worker goroutine when the timeout fires.
func predictableFunc(ctx context.Context) (int64, error) {
// your code here
}
Write the implementation.
Run unpredictableFunc in a goroutine that sends its result on a buffered channel of capacity 1, then select over that channel versus ctx.Done(). The buffer is load-bearing: with an unbuffered channel the worker blocks forever on the send after a timeout, leaking the goroutine. If ctx has no deadline, wrap it with context.WithTimeout and defer cancel().
- ✗Using an unbuffered channel, leaking the worker goroutine on timeout
- ✗Believing
contextcancellation propagates into an opaque blocking call automatically - ✗Forgetting
defer cancel()aftercontext.WithTimeout, leaking the timer
- →Why does an unbuffered result channel cause the worker to leak after a timeout?
- →Why does
context.WithTimeoutneed a matchingcancel()call?
Solution
func predictableFunc(ctx context.Context) (int64, error) {
if _, ok := ctx.Deadline(); !ok {
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, time.Second)
defer cancel()
}
resCh := make(chan int64, 1) // buffer 1 — else the worker leaks on timeout
go func() {
resCh <- unpredictableFunc()
}()
select {
case res := <-resCh:
return res, nil
case <-ctx.Done():
return 0, ctx.Err()
}
}
Why the buffer is mandatory. On a timeout the receiver leaves the select via ctx.Done() and never reads resCh again. With an unbuffered channel the worker's resCh <- ... send would block forever — the goroutine leaks. A buffer of 1 accepts the value with no receiver present, so the worker finishes.
We cannot interrupt unpredictableFunc (its body is off-limits), so the timeout only frees the caller; the background goroutine still runs to completion but does not hang.