Lewislbr

The context package in Go

The context package is used to carry deadlines, cancellation signals, and request-scoped values across processes. It is mostly used to enable cancellation of processes once they have started.

A context can be created in several ways, and can be cancelled automatically or manually using a cancel function. Whatever the case, a deferred call to cancel should exist, as it will free up resources related to that context once it is not used.

Example carying a value:

To avoid collisions between packages using context, a custom type definition should be used for the key.

type s string

const key s = "foo"

func main() {
	ctx := context.WithValue(context.Background(), key, "bar")

	value := ctx.Value(key)

	fmt.Println(value)
}

// Output:
// bar

By default, the type of the value will be interface{}. We can cast it to another type for better usage:

type s string

const key s = "foo"

func main() {
	ctx := context.WithValue(context.Background(), key, "bar")

	value, ok := ctx.Value(key).(string)
	if !ok {
		fmt.Println("not found")
		return
	}

	fmt.Println(value)
}

// Output:
// bar

Example with a timeout:

const (
	max     = 5000
	timeout = 3000 * time.Millisecond
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), timeout)
	defer cancel()

	result := make(chan string)

	go func() {
		fmt.Println("working on it...")
		result <- slowOperation()
	}()

	select {
	case r := <-result:
		fmt.Println(r)
	case <-ctx.Done():
		fmt.Println("operation timed out")
	}
}

func slowOperation() string {
	rand.Seed(time.Now().Unix())
	duration := rand.Intn(max)

	time.Sleep(time.Duration(duration) * time.Millisecond)

	return fmt.Sprintf("operation completed in %v milliseconds", duration)
}

Variant:

const (
	max     = 5000
	timeout = 3000 * time.Millisecond
)

func main() {
	_, cancel := context.WithCancel(context.Background())
	defer cancel()

	time.AfterFunc(timeout, func() {
		fmt.Println("operation timed out")
		cancel()
		os.Exit(0)
	})

	fmt.Println("working on it...")

	result := slowOperation()

	fmt.Println(result)
}

func slowOperation() string {
	rand.Seed(time.Now().Unix())
	duration := rand.Intn(max)

	time.Sleep(time.Duration(duration) * time.Millisecond)

	return fmt.Sprintf("operation completed in %v milliseconds", duration)
}

It's worth noting that the same behavior can be usually achieved without context:

const (
	max     = 5000
	timeout = 3000 * time.Millisecond
)

func main() {
	result := make(chan string)

	go func() {
		fmt.Println("working on it...")
		result <- slowOperation()
	}()

	select {
	case r := <-result:
		fmt.Println(r)
	case <-time.After(timeout):
		fmt.Println("operation timed out")
	}
}

func slowOperation() string {
	rand.Seed(time.Now().Unix())
	duration := rand.Intn(max)

	time.Sleep(time.Duration(duration) * time.Millisecond)

	return fmt.Sprintf("operation completed in %v milliseconds", duration)
}

Example capturing cancellation signals:

const max = 5000

func main() {
	_, cancel := context.WithCancel(context.Background())
	defer cancel()

	go func() {
		quit := make(chan os.Signal, 1)
		signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
		<-quit
		fmt.Println("operation cancelled")
		cancel()
		os.Exit(0)
	}()

	fmt.Println("Working on it...")

	result := slowOperation()

	fmt.Println(result)
}

func slowOperation() string {
	rand.Seed(time.Now().Unix())
	duration := rand.Intn(max)

	time.Sleep(time.Duration(duration) * time.Millisecond)

	return fmt.Sprintf("operation completed in %v milliseconds", duration)
}

When a function accepts a context as a parameter, by convention it should be the first parameter.



If you're using dark mode, do you like the code blocks's theme? I have it available for VS Code, feel free to check it.