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 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 :
$ 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
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.
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