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:
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 thedone 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
Contexthas many methods compared to the other interfaces of the standard library, which usually have one or two methods. Three of them are closely related:
- Deadlineis the time for cancelling
- Donesignals when thecontextis done
- Errreturns 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 :
$ 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?
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: -
$ 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.
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
Thecontext.WithValuefunction 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 avoidstringvalues because two different packages using context
could overwrite each other’s values. The suggestion is to use
user-defined concrete types such asstruct{}.
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.
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.


