Welcome To Golang By Example

Using Context Package in GO (Golang) – Complete Guide

Introduction:

Definition:

Context is a package provided by GO. Let’s first understand some problems that existed already, and which context package tries to solve.

Problem Statement:

If you notice all the above problems are quite applicable to HTTP requests and but none the less these problems are also applicable to many different areas too.

For a web HTTP request, it needs to be canceled when the client has disconnected, or the request has to be finished within a specified timeout and also requests scope values such as request_id needs to be available to all downstream functions.

When to Use (Some Use Cases):

Context Interface

The core of the understanding context is knowing the Context interface

type Context interface {
    //It retures a channel when a context is cancelled, timesout (either when deadline is reached or timeout time has finished)
    Done() <-chan struct{}

    //Err will tell why this context was cancelled. A context is cancelled in three scenarios.
    // 1. With explicit cancellation signal
    // 2. Timeout is reached
    // 3. Deadline is reached
    Err() error

    //Used for handling deallines and timeouts
    Deadline() (deadline time.Time, ok bool)

    //Used for passing request scope values
    Value(key interface{}) interface{}
}

Creating New Context

context.Background():

context package function Background() returns a empty Context which implements the Context interface

  1. It has no values
  2. It is never canceled
  3. It has no deadline

Then what is the use context.Background(). context.Background() serves as the root of all context which will be derived from it. It will be more clear as we go along

context.ToDo():

The above two methods describe a way of creating new contexts. More context can be derived from these contexts. This is where context tree comes into the picture

Context Tree

Before understanding Context Tree please make sure that it is implicitly created in the background when using context. You will find no mention of in go context package itself.

Whenever you use context, then the empty Context got from context.Background() is the root of all context. context.ToDo() also acts like root context but as mentioned above it is more like a context placeholder for future use. This empty context has no functionality at all and we can add functionality by deriving a new context from this. Basically a new context is created by wrapping an already existing immutable context and adding additional information. Let's see some example of a context tree which gets created

Two level tree

rootCtx := context.Background()
childCtx := context.WithValue(rootCtx, "msgId", "someMsgId")

In above

Three level tree

rootCtx := context.Background()
childCtx := context.WithValue(rootCtx, "msgId", "someMsgId")
childOfChildCtx, cancelFunc := context.WithCancel(childCtx)

In above

Multi-level tree

rootCtx := context.Background()
childCtx1 := context.WithValue(rootCtx, "msgId", "someMsgId")
childCtx2, cancelFunc := context.WithCancel(childCtx1)
childCtx3 := context.WithValue(rootCtx, "user_id", "some_user_id)

In above:

Above three-level tree would look like below

As since it is a tree, it is also possible to create more childs for a particular node. For eg we can derive a new context childCtx4 from childCtx1

childCtx4 := context.WithValue(childCtx1, "current_time", "some_time)

Tree with above node added would like below:

At this very moment, it might not be clear how WithValue() or WithCancel() function is used. Right now just understand that whenever using context, a context tree is created with root as the emptyCtx . These functions will get clear as we move on

Deriving From Context

A derived context is can be created in 4 ways

Let's understand each of the above in details

context.WithValue()

Used for passing request-scoped values. The complete signature of the function is

withValue(parent Context, key, val interface{}) (ctx Context)

It takes in a parent context, key, value and returns a derived context  This derived context has key associated with the value. Here the parent context can be either context.Background() or any other context. Further, any context which is derived from this context will have this value.

#Root Context
ctxRoot := context.Background() - #Root context 

#Below ctxChild has acess to only one pair {"a":"x"}
ctxChild := context.WithValue(ctxRoot, "a", "x") 

#Below ctxChildofChild has access to both pairs {"a":"x", "b":"y"} as it is derived from ctxChild
ctxChildofChild := context.WithValue(ctxChild, "b", "y") 

Example:

Complete Working example of withValue(). In the below example, we are injecting a msgId for each incoming request. If you notice in below program

package main

import (
    "context"
    "net/http"
    "github.com/google/uuid"
)

func main() {
    helloWorldHandler := http.HandlerFunc(HelloWorld)
    http.Handle("/welcome", inejctMsgID(helloWorldHandler))
    http.ListenAndServe(":8080", nil)
}

//HelloWorld hellow world handler
func HelloWorld(w http.ResponseWriter, r *http.Request) {
    msgID := ""
    if m := r.Context().Value("msgId"); m != nil {
        if value, ok := m.(string); ok {
            msgID = value
        }
    }
    w.Header().Add("msgId", msgID)
    w.Write([]byte("Hello, world"))
}

func inejctMsgID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        msgID := uuid.New().String()
        ctx := context.WithValue(r.Context(), "msgId", msgID)
        req := r.WithContext(ctx)
        next.ServeHTTP(w, req)
        
    })
}

Simply do a curl call to the above request after running the above program

curl -v http://localhost/welcome

Here will be the response. Notice the MsgId that gets populated in the response headers. The injectMsgId function acts as middleware and injects a unique msgId to the request context.

curl -v http://localhost:8080/welcome
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /do HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> 
< HTTP/1.1 200 OK
< Msgid: a03ff1d4-1464-42e5-a0a8-743c5af29837
< Date: Mon, 23 Dec 2019 16:51:01 GMT
< Content-Length: 12
< Content-Type: text/plain; charset=utf-8
< 
* Connection #0 to host localhost left intact

context.WithCancel()

Used for cancellation signals. Below is the signature of WithCancel() function

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

context.WithCancel() function returns two things

Only the creator of this context should call the cancel function. It is highly not recommended to pass around the cancel function. Lets understand withCancel with an example.

Example:

package main

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

func main() {
    ctx := context.Background()
    cancelCtx, cancelFunc := context.WithCancel(ctx)
    go task(cancelCtx)
    time.Sleep(time.Second * 3)
    cancelFunc()
    time.Sleep(time.Second * 1)
}

func task(ctx context.Context) {
    i := 1
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Gracefully exit")
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Println(i)
            time.Sleep(time.Second * 1)
            i++
        }
    }
}

Output:

1
2
3
Gracefully exit
context canceled

In the above program

context.WithTimeout()

Used for time-based cancellation. The signature of the function is

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

context.WithTimeout() function  will

Lets see an example

Example:

package main

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

func main() {
    ctx := context.Background()
    cancelCtx, cancel := context.WithTimeout(ctx, time.Second*3)
    defer cancel()
    go task1(cancelCtx)
    time.Sleep(time.Second * 4)
}

func task1(ctx context.Context) {
    i := 1
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Gracefully exit")
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Println(i)
            time.Sleep(time.Second * 1)
            i++
        }
    }
}

Output:

1
2
3
Gracefully exit
context deadline exceeded

In the above program

context.WithDeadline()

Used for deadline-based cancellation. The signature of the function is

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

context.WithDeadline() function

Let's see an example

Example:

package main

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

func main() {
    ctx := context.Background()
    cancelCtx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second*5))
    defer cancel()
    go task(cancelCtx)
    time.Sleep(time.Second * 6)
}

func task(ctx context.Context) {
    i := 1
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Gracefully exit")
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Println(i)
            time.Sleep(time.Second * 1)
            i++
        }
    }
}

Output:

1
2
3
4
5
Gracefully exit
context deadline exceeded

In the above program


What We Learned

How to create the context:

Context Tree

Deriving a new context

BestPractices and Caveats

Following is a list of best practices that you can follow while using a context.