When you enter the world of Go Language, you must be attracted by the powerful Concurrency, which can be used to drop tasks into the background with the simplest keyword go, but how to control Concurrency efficiently is a must-learning for Go language. This article introduces three ways to get acquainted with Concurrency, which correspond to three different terms: WaitGroup, Channel, and Context.

WaitGroup

Suppose you have two machines that need to upload the latest code at the same time, and the last restart step can be executed only after the two machines have finished uploading separately. It’s like splitting a job into several parts and doing it at the same time to reduce the time, but then you need to wait until all the jobs are done before you can execute the next step, so you need to use the WaitGroup function to do that. Here’s a simple 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
26
27
28
29
30
package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    i := 0
    wg.Add(3) //task count wait to do
    go func() {
        defer wg.Done() // finish task1
        fmt.Println("goroutine 1 done")
        i++
    }()
    go func() {
        defer wg.Done() // finish task2
        fmt.Println("goroutine 2 done")
        i++
    }()
    go func() {
        defer wg.Done() // finish task3
        fmt.Println("goroutine 3 done")
        i++
    }()
    wg.Wait() // wait for tasks to be done
    fmt.Println("all goroutine done")
    fmt.Println(i)
}

Channel

Another practical case is that we need to actively notify a Goroutine to stop. For example, when the app is started, it will run some monitors in the background, and when the whole app needs to stop, we need to send a Notification to the monitors in the background to stop it first, and then we need to use Channel to notify them. Here’s an 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"
    "time"
)

func main() {
    exit := make(chan bool)
    go func() {
        for {
            select {
            case <-exit:
                fmt.Println("Exit")
                return
            case <-time.After(2 * time.Second):
                fmt.Println("Monitoring")
            }
        }
    }()
    time.Sleep(5 * time.Second)
    fmt.Println("Notify Exit")
    exit <- true //keep main goroutine alive
    time.Sleep(5 * time.Second)
}

The above example shows that the background is controlled by a Goutine and a Channel. As you can imagine, when there are many Goroutines in the background, we need to declare multiple Channels to control them. Maybe there will be another Goroutine inside the Goroutine, and the developer will find that it is no longer possible to control multiple Goroutines by simply using a Channel. The solution is to use context.

Context

You can imagine that today there is a background task A, and task A generates task B, and task B generates task C. That is, we can follow this pattern all the way through. Suppose we need to stop task A in the middle, and A has to tell B and C to stop together, then context is the fastest way.

 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
34
35
36
37
38
39
40
41
package main

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

func foo(ctx context.Context, name string) {
    go bar(ctx, name) // A calls B
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name, "A Exit")
            return
        case <-time.After(1 * time.Second):
            fmt.Println(name, "A do something")
        }
    }
}

func bar(ctx context.Context, name string) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println(name, "B Exit")
            return
        case <-time.After(2 * time.Second):
            fmt.Println(name, "B do something")
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go foo(ctx, "FooBar")
    fmt.Println("client release connection, need to notify A, B exit")
    time.Sleep(5 * time.Second)
    cancel() //mock client exit, and pass the signal, ctx.Done() gets the signal  time.Sleep(3 * time.Second)
    time.Sleep(3 * time.Second)
}

You can think of context as a controller that can control an indeterminate number of goroutines at any time, and from top to bottom, you can stop the whole background service by cancel() at any point in time after declaring `context. This side case will be used when the App needs to be restarted, all goroutines will be notified to stop first, and the App will be restarted only after it stops normally.

Conclusion

Choose different functions according to different contexts and situations, here is a summary

  • WaitGroup: If you need to split a single Job into multiple sub-tasks and wait until all of them are completed before proceeding to the next step, this is the most suitable time to use WaitGroup.
  • Channel+select: Channel can only be used in the case of a single Goroutine. If you want to manage multiple Goroutines, it is recommended to use context.
  • Context: If you want to control all the Goroutines at once, I believe context is the most suitable, and it is also the most used by Go. Of course, there are other features of context, please refer to the official documentation for details.