Context was officially included in the official standard library in Go language version 1.7. Why do we introduce the use of context today? The reason is simple. When learning Go for the first time and writing APIs, you will often see that the first parameter in the http handler is ctx context.Context, and what exactly is the purpose and meaning of this context used here? This article is to bring you to understand what context is and how it is used. The content will not mention the source code of context, but use a few practical examples to understand it.

Using WaitGroup

When learning Go, you must learn how to use concurrency (goroutine), and how should developers control concurrency? In fact, there are two ways, one is WaitGroup, and the other is context. When you need to split the same thing into different Jobs to execute, and finally you need to wait until all the Jobs are finished before continuing to execute the main program, then you need to use WaitGroup, see an actual example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup

    wg.Add(2)
    go func() {
        time.Sleep(2 * time.Second)
        fmt.Println("job 1 done.")
        wg.Done()
    }()
    go func() {
        time.Sleep(1 * time.Second)
        fmt.Println("job 2 done.")
        wg.Done()
    }()
    wg.Wait()
    fmt.Println("All Done.")
}

The above example shows that the main program uses wg.Wait() to wait for all jobs to finish running before printing the final message. There is a situation where the job is split into multiple jobs and left to run in the background, but how can the user terminate the related goroutine work in other ways (e.g. developers write background programs to monitor and take a long time to run)? For example, if there is a stop button on the UI, after clicking it, how to actively notify and stop the running Job, this is very simple, you can use channel + select method.

Using channel + select

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
    "fmt"
    "time"
)

func main() {
    stop := make(chan bool)

    go func() {
        for {
            select {
            case <-stop:
                fmt.Println("got the stop channel")
                return
            default:
                fmt.Println("still working")
                time.Sleep(1 * time.Second)
            }
        }
    }()

    time.Sleep(5 * time.Second)
    fmt.Println("stop the gorutine")
    stop <- true
    time.Sleep(5 * time.Second)
}

As you can see above, the problem can be solved quickly by select + channel, just drop the bool value into the stop channel anywhere to stop the Job being processed in the background. The above problem is solved by using channel, but now there is a problem. Suppose there are several goroutines running in the background, or there are goroutines running in the goroutine, it becomes quite complicated, such as the situation below.

go channel

There is no way to handle this in a channel way, instead we need to use the context that is the focus of today’s session.

Understanding context

As you can see from the above diagram, we have created three worker nodes to handle different Jobs, so we declare a main context.Background() at the top of the main program, and then create sub-contexts in each worker node, the main purpose of which is to cancel the running Job in that worker directly when one of the contexts is closed. Take the above example and rewrite it.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

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

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

    go func() {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("got the stop channel")
                return
            default:
                fmt.Println("still working")
                time.Sleep(1 * time.Second)
            }
        }
    }()

    time.Sleep(5 * time.Second)
    fmt.Println("stop the gorutine")
    cancel()
    time.Sleep(5 * time.Second)
}

In fact, you can see that only the original channel is replaced by the use of context to deal with the other completely unchanged, this side mentioned the use of context.WithCancel, the use of the following way to expand the context.

1
ctx, cancel := context.WithCancel(context.Background())

The idea is that each worknode has its own cancel func and the developer can call cancel() elsewhere to decide which worker needs to be stopped, so that the effect of using context to stop multiple goroutines can be achieved.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package main

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

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

    go worker(ctx, "node01")
    go worker(ctx, "node02")
    go worker(ctx, "node03")

    time.Sleep(5 * time.Second)
    fmt.Println("stop the gorutine")
    cancel()
    time.Sleep(5 * time.Second)
}

func worker(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name, "got the stop channel")
            return
        default:
            fmt.Println(name, "still working")
            time.Sleep(1 * time.Second)
        }
    }
}

The above can stop multiple workers at once through a context, depending on how the logic declares the context and what time to execute cancel(), usually I personally pair it with graceful shutdown to cancel the running Job, or stop the database connection, etc…

Summary

When you first learn Go, if you don’t use goroutine often, you won’t understand how and when to use context until you need to have background processing and how to stop a Job. Of course, context is not the only way to use it, and we will introduce other ways to use it in the future.