Timeout using context package in Go
Tags: go
Published: 2021-12-28 04:27:05

## Key takeaways

- [context.WithTimeout](https://pkg.go.dev/context#WithTimeout) can be used in a timeout implementation.
- [WithDeadline](https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/context/context.go;l=434;drc=refs%2Ftags%2Fgo1.17.5) returns [CancelFunc](https://pkg.go.dev/context#CancelFunc) that tells an operation to abandon its work.
- timerCtx implements `cancel()` by stopping its timer then delegating to [cancelCtx.cancel](https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/context/context.go;l=397), and cancelCtx closes the context.
- ctx.Done returns a channel that's closed when work done on behalf of this context should be canceled.


## context.WithTimeout

The context package as the standard library was moved from the golang.org/x/net/context package in [Go 1.7](https://tip.golang.org/doc/go1.7#context). This allows the use of contexts for cancellation, timeouts, and passing request-scoped data in other library packages.

[context.WithTimeout](https://pkg.go.dev/context#WithTimeout) can be used in a timeout implementation.

```go
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
```

For example, you could implement it as follow ([Go playground](https://go.dev/play/p/lEEfym9gs25)):

```go
package main

import (
	"context"
	"fmt"
	"log"
	"time"
)

func execute(ctx context.Context) error {
	proc1 := make(chan struct{}, 1)
	proc2 := make(chan struct{}, 1)

	go func() {
		// Would be done before timeout
		time.Sleep(1 * time.Second)
		proc1 <- struct{}{}
	}()

	go func() {
		// Would not be executed because timeout comes first
		time.Sleep(3 * time.Second)
		proc2 <- struct{}{}
	}()

	for i := 0; i < 3; i++ {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-proc1:
			fmt.Println("process 1 done")
		case <-proc2:
			fmt.Println("process 2 done")

		}
	}

	return nil
}

func main() {
	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()

	if err := execute(ctx); err != nil {
		log.Fatalf("error: %#v\n", err)
	}
	log.Println("Success to process in time")
}
```

Canceling this context releases resources associated with it, so you should call cancel as soon as the operations running in this Context complete.

```go
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
```

Cancel notification after timeout is received from [ctx.Done()](https://pkg.go.dev/context#Context). Done returns a channel that's closed when work done on behalf of this context should be canceled. WithTimeout arranges for Done to be closed when the timeout elapses.

```go
select {
case <-ctx.Done():
	return ctx.Err()
}
```

When you execute this code, you will get the following result. A function call that can be completed in 1s will be finished, but a function call that can be done after 3s will not be executed because a timeout occurs in 2s.

```bash
$ go run main.go
process 1 done
2021/12/28 12:32:59 error: context.deadlineExceededError{}
exit status 1
```

In this way, you can implement timeout easily.

## Deep dive into context.WithTimeout
 
Here's a quick overview.

![A diagram describing the inside of context package](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vh5ni7ikyj7lsrtwzx2l.png)

[WithTimeout](https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/context/context.go;l=506) is a wrapper function for [WithDeadline](https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/context/context.go;drc=refs%2Ftags%2Fgo1.17.5;l=434).

```go
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}
```

[WithDeadline](https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/context/context.go;l=434;drc=refs%2Ftags%2Fgo1.17.5) returns [CancelFunc](https://pkg.go.dev/context#CancelFunc) that tells an operation to abandon its work. Internally, a function that calls `timerCtx.cancel()`, a function that's not exported, will be returned. 

```go
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	// (omit)
	c := &timerCtx{
		cancelCtx: newCancelCtx(parent),
		deadline:  d,
	}
	// (omit)
	return c, func() { c.cancel(true, Canceled) }
}

// (omit)

type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}
```

A [timerCtx](https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/context/context.go;drc=refs%2Ftags%2Fgo1.17.5;l=465) carries a timer and a deadline, and embeds a [cancelCtx](https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/context/context.go;l=342;drc=refs%2Ftags%2Fgo1.17.5;bpv=1;bpt=1).

```go
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
	children map[canceler]struct{} // set to nil by the first cancel call
	err      error                 // set to non-nil by the first cancel call
}
```

timerCtx implements `cancel()` by stopping its timer then delegating to [cancelCtx.cancel](https://cs.opensource.google/go/go/+/refs/tags/go1.17.5:src/context/context.go;l=397).

```go
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
	c.err = err
	d, _ := c.done.Load().(chan struct{})
	if d == nil {
		c.done.Store(closedchan)
	} else {
		close(d)
	}
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)
	}
	c.children = nil
	c.mu.Unlock()

	if removeFromParent {
		removeChild(c.Context, c)
	}
}
```

In the function the context is closed.

## Conclusion

I explained how to implement timeout with context package, and dived into internal implementation in it.  I hope this helps you understand the Go implementation.