loading...
Cover image for A deep dive into Go's Context Package

A deep dive into Go's Context Package

ghvstcode profile image Ghvst Code ・10 min read

The Go context package was developed to make it easy to pass request-scoped values, deadlines, and cancellation signals across API boundaries and processes. Thinking about Context in non-technical terms("the circumstances that form the setting for an event, statement, or idea, and in terms of which it can be fully understood") for a start could help us better understand Context as used technically. The context package comes in very handy when working with servers, making HTTP requests, and a host of other activities involving goroutines. This article aims to explain the context package Go provides in-depth. In the coming sections, we would be diving deep into the context package.

Prerequisites

Before you continue reading you'll need :

  • Go installed on your computer, depending on your os you can follow the following articles to get started with it Mac, Windows,Ubuntu
  • A little Knowledge of concurrency in Go, You can check out thisarticle to know more

The Context Type

The main component of the context package is the context type. We can learn more about this type from the Go documentation. Looking at it closely we can notice that the context type is an interface that implements four functions, the deadline function, the done function, the error function, and finally the value function. These functions are the fundamentals for most of the operations we would be carrying out with the context package, we would see their applications later on in this article, but it is important to know that the context package provides us a context type that is the building block of the context package. The second type within the context package is the type CancelFunc which is a function. CancelFunc is used for cancellation of operations, it doesn't wait for these operations to stop before canceling, it tells the operations to abandon whatever they are doing.
Context in Action
The context package allows us to create new context values from the already existing ones, using a couple of functions it provides to us! These functions are the WithCancel, WithTimeout, and WithDeadline functions, they all return two values of type Context and type cancelFunc respectively. The values these functions return are called derived values and they usually form a tree with the context provided by the Background function as the root. When a context is canceled all the values derived from it are automatically canceled. The Background function is typically used for initialization purposes as it returns a non-nil, empty context and is never canceled, even though the withCancel, WithTimeout, and WithDeadline functions return a child context that can be canceled and have its reference to the Background context removed.
Valid-Context Image
The tree formed by these derived context values allow for cancellation propagation, a scenario whereby if a context is canceled all contexts derived from it would also be canceled, This is important to note because cancellation is one of the most common uses of the context package. It is a rule of thumb when working with contexts to avoid storing it in a struct as this may affect the synchronous nature of the program, storing the context in a struct implies that all the methods on that struct use that context and this might result in chaos when those methods are called from functions with a different context, remember that the context values should be request scoped. Ensure that a Context is passed explicitly to a function that takes it as an argument, the context passed to the function should typically be the first parameter and is not nil. A nil context should not be passed to a function. If you do not know the current value of the context you can pass the value of the ToDo function
Similar to the Background function, the ToDo function returns a non-nil empty context that should be used when one is unsure of what context to use or as a placeholder when the function surrounding it has not yet received a context. The Background and ToDo functions provide the base on which more context values can be derived. The first way of deriving contexts we would look at is using the withValue function.
WithValue Function
The withValue function as the name implies is used while working with values, these values are usually request-scoped and are typically for data that transits processes and APIs. The withValue function should not be used for passing parameters to functions. The withValue Function is defined as below func WithValue(parent Context, key, val interface{}) Context it takes in a parent/Root context, a key, and a value to be associated with the key. It returns a Context that contains a value with that key, it works similar to a key-value pair model. It is important to note that the key should not be of any inbuilt type, it should be a custom type this is to avoid collisions between packages using context.

package main 

import (
    "fmt"
    "context"
)

type keyType string

func main() {
    key := keyType("Name")
    ctx := context.WithValue(context.Background(), key, "Tobyy")
    exampleContext(ctx, key)
}

func exampleContext(ctx context.Context, k keyType){
    value := ctx.Value(k)
    if value != nil {
        fmt.Print("The context value is :", value)
        return
    }
    fmt.Print("Ooooops, unable to find the context value")
}

The example above is a simple program with two functions, the default main function and a second custom function called exampleContext. The exampleContext function expects a context as its first argument and a key for the context value. Note that the type of the context value is not an inbuilt type as we had discussed earlier. The purpose of the exampleContext function is to check the provided context if there is a value who's key corresponds to the provided key. If the value is present, we print out the value else we print out a message to the terminal saying the context value is not found. Pretty simple right? the real fun part happens in the main function. In the main function, we create a variable whose value is of type keyType(keyType is the custom type created), in this example, I named the variable key for clarity purposes but it can be named as it suits you, this variable will then be passed to the exampleContext function. The second argument the exampleContext function expects is a context of type Context. we create this context in the second line of our main function. This function is created using the withValue function because we are dealing with values. If you remember from up above we said the withValue function requires a parent Context as one of its arguments, so we pass in the background context gotten from the function Background as the first argument to exampleContext. we can abstract the call to context.Background into a variable and pass it to the withValue function but for the sake of brevity, we simply invoke it right there as an argument. The next argument the withValue function expects is the key, so we pass in the key created in the line above as a parameter. Another argument the withValue function expects is the value itself that we would like to attach to the key. For this simple example, I would pass in in the string "Toby" as the value. Lastly, we pass in the context we created as an argument to the exampleContext function and we can go ahead and run it.
If we run the program, we get the output as below

The context value is: Toby

This is amazing, we get the value printed out! The withValue function is very essential for passing values, especially request-scoped ones. One of the most common use cases of the withValue function is in making HTTP requests. Go's net/Http package has machinery under the hood that creates a context for each request and can be accessed with the Context() method, so it is common to see r.Context() where r is of type http.Request.

WithCancel Function

Another way to get a derived context is by using the withCancel function, the withCancel function just like the rest of the other functions used for deriving context returns a child context and a cancel Function. However, the child context returned by the withCancel function comes with a new Done() channel, remember we discussed above the Context interface and saw that it has a Done() function in it? The returned contexts Done channel is closed when either the returned cancel Func is called or its parent's contexts Done channel is closed depending on which of these happens first. whenever the Done channel is closed, an error is returned by the context.err function with the message "Context Cancelled". Let's take a look at an example to help us further understand the withCancel function.

package main

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

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

    time.AfterFunc(2*time.Second, cancel)

    sayMyName(ctx, 5*time.Second, "Toby")
}

func sayMyName(ctx context.Context, d time.Duration, name string){
    select {
    case <- time.After(d):
        fmt.Print("Your name is ", name)
    case <-ctx.Done():
        err := ctx.Err()
       fmt.Print(err)
    }
}

The example above is one commonly used in explaining the concept of Context in Go, in there, there are two functions. The first function in the example above is the main function, the second function is a custom function we created that takes three arguments, the context of type Context, the duration of type time.Duration and lastly your name of type string. The end goal of the sayMyName function is to print out your Name after the duration of time provided IF it is not canceled by the context. Inside of the sayMyName function, there is a select statement. The select statement in Go is used to allow a goroutine wait on multiple communication operations. A select statement blocks until one of its cases can run, then it executes that case. The first case we are using the select on in the example above is if the duration has passed and for this, we use the time. After function that takes in a duration waits for the duration to elapse and then sends a message on the returned channel. The second case is if the Done function returns a closed channel remember we discussed earlier that when the cancel returned by the WithCancel function is invoked, it arranges for done to be closed. These are the two cases we are checking for and if either can run, then the case is executed. Going back to the main function, we attempt to call the sayMyName Function, but one of the arguments it takes is the context, so we use the WithCancel function and pass in the Background Context as its root. The WithCancel function returns a new context and cancel, so we store them in variables. Next, we pass the derived context to the sayMyName function alongside a duration of five seconds and the string "Toby", Lastly we need to invoke the cancel that the WithCancel function returns and to do that we use the AfterFunc function the time package provides. The AfterFunc Function says hey I will invoke the function passed to me after the time you want to wait. For the sake of this example I use two seconds to simulate a delay, and now we are done setting up the function we can go ahead and run it.

context canceled

It worked as desired, the string passed in never gets printed out! This is because we canceled this function after two seconds while your name was supposed to print after five seconds. Amazing right? that is the power the context package gives us. The ability to carry out cancellation and cancellation propagation. This can very important especially when working servers and making network requests, a user can decide to cancel their request before the response and that would happen gracefully because Canceling the context releases resources associated with it. The WithTimeOut is basically a withCancel function on steroids, we would look into it in the next section

WithTimeOut Function

package main

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

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

    sayMyName(ctx, 5*time.Second, "Toby")
}

func sayMyName(ctx context.Context, d time.Duration, name string){
    select {
    case <- time.After(d):
        fmt.Print("Your name is ", name)
    case <-ctx.Done():
        err := ctx.Err()
        fmt.Print(err)
    }
}

The example above is pretty similar to the one in the previous section. however, in this example, we are making use of the WithTimeout function. The WithTimeOut Function is most frequently used when making HTTP requests to a server and for similar activities. The WithTimeOut function Accepts a timeout duration after which the done channel will be closed and context will be canceled it returns a new child Context and a cancel function that can be called in case the context needs to be canceled before timeout. On the second line of the main function, we derive a context by passing in the background context and the duration, which in this case is two seconds. On the line below that, we defer a call to cancel! you might notice that in this example the AfterFunc function is gone this is because the WithTimeOut function now does the job of the Afterfunc function and does so elegantly if we run this example, we get the output as shown below which is great!

context deadline exceeded

Notice how the error message changed? that error is associated with the deadline function we saw defined in the context interface up above!
The Context Package provides us with so much essential functionality that would help when writing GO programs, I would suggest you take a look at the Context packages source code which is only about 554 lines long to help solidify your knowledge of Context All the examples in this article could be found here.

If you enjoyed this article and feel up to it, please do share it! If you have any questions, leave a comment! I'll be here to answer it! You can connect with me on twitter

Posted on by:

ghvstcode profile

Ghvst Code

@ghvstcode

* learning to become a Back-end engineer

Discussion

markdown guide