In daily development we will probably encounter timeout control scenarios, such as a batch of time-consuming tasks, network requests, etc.; a good timeout control can effectively avoid some problems (such as goroutine leakage, resource non-release, etc.).

Timer

The first option is Time.After(d Duration): the timeout control in go is very simple.

1
2
3
4
5
func main() {
	fmt.Println(time.Now())
	x := <-time.After(3 * time.Second)
	fmt.Println(x)
}

output:

1
2
2021-10-27 23:06:04.304596 +0800 CST m=+0.000085653
2021-10-27 23:06:07.306311 +0800 CST m=+3.001711390

Context

The second option is to use context, which is a powerful feature of go.

Using the context.WithTimeout() method will return a context with a timeout function.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ch := make(chan string)
timeout, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
go func() {
	time.Sleep(time.Second * 4)
	ch <- "done"
}()
select {
case res := <-ch:
	fmt.Println(res)
case <-timeout.Done():
	fmt.Println("timout", timeout.Err())
}

In the same way, the Done() function of context returns a channel that will take effect when the current job is done or when the context is cancelled.

1
timout context deadline exceeded

The timeout.Err() method also tells the reason for the current context closure.

goroutine Passing context

Another advantage of using context is that you can take advantage of the fact that it is naturally passed across multiple goroutines, so that all goroutines that pass the context receive cancellation notifications at the same time, which is very widely used in multiple go.

 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
func main() {
	total := 12
	var num int32
	log.Println("begin")
	ctx, cancelFunc := context.WithTimeout(context.Background(), 3*time.Second)
	for i := 0; i < total; i++ {
		go func() {
			//time.Sleep(3 * time.Second)
			atomic.AddInt32(&num, 1)
			if atomic.LoadInt32(&num) == 10 {
				cancelFunc()
			}
		}()
	}
	for i := 0; i < 5; i++ {
		go func() {
			select {
			case <-ctx.Done():
				log.Println("ctx1 done", ctx.Err())
			}
			for i := 0; i < 2; i++ {
				go func() {
					select {
					case <-ctx.Done():
						log.Println("ctx2 done", ctx.Err())
					}
				}()
			}
		}()
	}
	time.Sleep(time.Second*5)
	log.Println("end", ctx.Err())
	fmt.Printf("执行完毕 %v", num)
}

In the above example, no matter how many layers of goroutine are nested, it is possible to get a message when context is cancelled (provided that context is passed away, of course)

You can also call the cancelFunc() function manually when you need to cancel the context in advance in some special cases.

Case in Gin

The Shutdown(ctx) function provided by Gin also makes full use of the context.

1
2
3
4
5
6
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
	log.Fatal("Server Shutdown:", err)
}
log.Println("Server exiting")

For example, the above code is to wait 10s for Gin to release its resources, which is the same principle as the above example.