Golang Context Complete Tutorial with Examples

Introduction to Golang Context

What is Go context? The context.Context package defined as a type called Context that defines and carries Deadlines, Cancel Signals and other Request-scoped Values in a chained model. If we look at the official documentation it says,

Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.

The main entity in the package is Context itself, which is an interface. It has only four methods:

Advertisement
type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}

Here,

  • Deadline: Returns the time when the context should be cancelled, together with a Boolean that is false when there is no deadline
  • Done: Returns a receive-only channel of empty structs, which signals when the context should be cancelled
  • Err: Returns nil while the done channel is open; otherwise it returns the cause of the context cancellation
  • Value: Returns a value associated with a key for the current context, or nil if there's no value for the key

Context has many methods compared to the other interfaces of the standard library, which usually have one or two methods. Three of them are closely related:

  • Deadline is the time for cancelling
  • Done signals when the context is done
  • Err returns the cause of the cancellation

The last method, Value, returns the value associated with a certain key. The rest of the package is a series of functions that allow you to create different types of contexts.

 

Why Use Context?

  • It simplifies the implementation for deadlines and cancellation across your processes or API.
  • It prepares your code for scaling, for example, using Context will make your code clean and easy to manipulate in the future by chaining all your process in a child parent relationship, you can tie/join any process together.
  • it’s easy to use.
  • Goroutine safe, i.e you can run the same context on different goroutines without leaks.

 

How to Use Go Background Context

Background is an empty context that doesn't get cancelled, hasn't got a deadline, and doesn't hold any values. It is mostly used by the main function as the root context or for testing purposes. We need to dig deep a little for better understanding.

Let’s look at it in our code editor: -

package main

import (
	"context"
	"fmt"
)

func main() {
	ctx := context.Background()
	fmt.Println(ctx)
}

Output :

Advertisement
$ go run main.go
context.Background

The output is “context.Background”, it shows us that it’s an empty Context which is an interface, on this Context interface, 

All these data are nil or empty currently because we have an empty Context the Background context which is never canceled, has no deadline and has no values. Background is typically used in main, init, and tests and as the top-level Context for incoming requests.

To check lets fmt.Println on all of them :

package main

import (
	"context"
	"fmt"
)

func main() {
	ctx := context.Background()
	fmt.Println("ctx.Err() : ", ctx.Err())
	fmt.Println("ctx.Done() : ", ctx.Done())
	fmt.Println("ctx.Value(\"key\") : ", ctx.Value("key"))
	fmt.Print("ctx.Deadline() : ")
	fmt.Print(ctx.Deadline())
}

Output:

$ go run main.go
ctx.Err() :  <nil>
ctx.Done() :  <nil>
ctx.Value("key") :  <nil>
ctx.Deadline() : 0001-01-01 00:00:00 +0000 UTC false

Here is another example of context.Background using go ticker:

package main

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

func main() {
	ctx := context.Background()
	done := ctx.Done()
	for i := 0; ; i++ {
		select {
		case <-done:
			return
		case <-time.After(time.Second):
			fmt.Println("tick", i)
		}
	}
}

We can see that, in the context of the example, the loop goes on infinitely because the context is never completed.

# go run main.go 
tick 0
tick 1
tick 2
tick 3
tick 4
tick 5
^Csignal: interrupt

So, lets check how to use this interface and other methods of Go Context package to handle and manage server function and processes.

 

Deadlines

Context Deadlines can be used in two ways: -

  • Context.WithDeadline
  • Context.WithTimeout

 

Context.WithDeadline

To use the context.WithDeadline we need a parent of type context.Context and d a type of time.Time , which will return a type of context.Context and a context.CancelFunc.

Code with example: -

package main

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

const shortDuration = 1 * time.Millisecond

func main() {
	d := time.Now().Add(shortDuration)
	ctx, cancel := context.WithDeadline(context.Background(), d)

	// Even though ctx will be expired, it is good practice to call its
	// cancellation function in any case. Failure to do so may keep the
	// context and its parent alive longer than necessary.
	defer cancel()
	select {
	case <-time.After(1 * time.Second):
		fmt.Println("overslept")
	case <-ctx.Done():
		fmt.Println(ctx.Err())
	}
}

Output:

context deadline exceeded

What’s happening?

Advertisement

We assigned a duration of 1 milliseconds to shortDuration, then in our main func(), assigned d to a time.Now() with the shortDuration to set a duration of 1milliseconds.To use context.WithDeadline(), we assigned a new context.Background() and our set duration d as the arguments which will return a ctx and a cancelfunc.

To test how this is working, we use Go Select to spin up two goroutines that will compare Context deadlines and  time.After.

The output in our example is “context deadline exceeded” because our set duration for the context is 1 milliseconds and the time.After case in our select is set to 1 seconds. If we were to increase our duration to above 1 seconds, our code output would be overslept because in that case, time.After would’ve finished counting.

 

Context.WithTimeout

To use the context.WithTimeout we need a parent of type context.Context and d a type of time.Duration , which will return a type of context.Context and a context.CancelFunc.

Code with example: -

package main

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

const shortDuration = 1 * time.Millisecond

func main() {

	// Pass a context with a timeout to tell a blocking function that it
	// should abandon its work after the timeout elapses.
	ctx, cancel := context.WithTimeout(context.Background(), shortDuration)
	defer cancel()
	select {
	case <-time.After(1 * time.Second):
		fmt.Println("overslept")
	case <-ctx.Done():
		fmt.Println(ctx.Err()) // prints "context deadline exceeded"
	}

}

Output: -

Advertisement
$ go run main.go
context deadline exceeded

Going through the code, there are some similarities among context.WithDeadline and context.WithTimeout, the difference here is that you have to pass in a duration of type time.Duration when using WithTimeout to end the Context. Our output is context deadline exceeded because we set the timeout to 1millisecond and the time.After in our select block will run for 1second.

 

Cancellation Signals

When we were using the Deadlines, there’s always a cancel() returned as part of the deadlines, it’s available on both so we can stop the context manually when needed, to use the cancel function directly you need a context.WithCancel() which is a type of context that has no timeout or time duration deadline.

 

Context.WithCancel

To use the context.WithCancel we need a parent of type context.Context which will return a type of context.Context and a context.CancelFunc.

Code with example: -

package main

import (
	"context"
	"fmt"
)

func main() {

	// gen generates integers in a separate goroutine and
	// sends them to the returned channel.
	// The callers of gen need to cancel the context once
	// they are done consuming generated integers not to leak
	// the internal goroutine started by gen.
	gen := func(ctx context.Context) <-chan int {
		dst := make(chan int)
		n := 1
		go func() {
			for {
				select {
				case <-ctx.Done():
					return // returning not to leak the goroutine
				case dst <- n:
					n++
				}
			}
		}()
		return dst
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // cancel when we are finished consuming integers
	for n := range gen(ctx) {
		fmt.Println(n)
		if n == 5 {
			break
		}
	}
}

Output: -

$ go run main.go
1
2
3
4
5

In the above code, gen generates integer n in a separate goroutine ands sends them to the returned channel, to use the gen method in this example, you need to cancel the context immediately they are consuming the integers so there won’t be any leakage in the goroutines internally.

Advertisement

This example above shows how you can use the WithCancel to efficiently close a goroutine without breaking your flow and with zero leakage, you will likely see this or have used it when dealing with graceful shutdown.

 

Request-Scoped Values

The context.WithValue function creates a copy of the parent context that has the given key associated with the specified value. Its scope holds values that are relative to a single request while it gets processed and should not be used for other scopes, such as optional function parameters.

The key should be something that can be compared, and it's a good idea to avoid string values because two different packages using context could overwrite each other's values. The suggestion is to use user-defined concrete types such as struct{}.

 

Context.WithValue

Let’s see how to use it in a simple way: -

package main

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

type keystore string
const sessionID keystore = "SESSIONID"

func main() {
	ctx := context.WithValue(context.Background(), sessionID, "SESSIONID_ session id")
	sessionID := ctx.Value(sessionID)
	str, ok := sessionID.(string)
	if !ok {
		log.Fatalln("not a string")
	}
	fmt.Println("value: ", str)
}

Output: -

$ go run main.go
value:  SESSIONID_ session id

After creating a context.WithValue, passed in parent context.Background and added a sessionID key with the value of the SESSIONID , by joining this context to all our request, we can easily grab the sessionID and even force a cancel or timeout easily, this is the type of control you have when you use Context in your request and response.

Advertisement

 

Best Practice when using Context

  • Whenever you need to use context.Context, make sure it always the first argument .
  • Always use “ctx” it will work perfectly if you use another variable name but just follow the majority, you don’t need to be unique with things like this.
  • Make sure the cancel function is called.
  • Don’t use a struct to add a context in a method, always add it to the argument i.e context.Context.
  • Don’t overuse context.WithValue.

 

Summary

We have learned that the Go Context package is used to handle flow of request in our processes or API, by chaining child context with the parent context, we can control the deadlines and cancellation signals in the chain by using the context.WithDeadline or context.WithTimeout method.

We also saw how to use the context.WithValue to add a key value to our context and how to retrieve any value from our context using the key. Then we listed some best practices when using the context.Context package in your Golang application.

 

References

https://pkg.go.dev/context

 

Didn't find what you were looking for? Perform a quick search across GoLinuxCloud

If my articles on GoLinuxCloud has helped you, kindly consider buying me a coffee as a token of appreciation.

Buy GoLinuxCloud a Coffee

For any other feedbacks or questions you can either use the comments section or contact me form.

Thank You for your support!!

Leave a Comment

X