2

Preface

In daily development, we will most likely 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 unreleased, etc.).

Timer

The method of implementing timeout control in go is very simple. First of all, the first solution is Time.After(d Duration) :

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

output:

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

time.After() will return a Channel , and the Channel will write data after a delay of d.

With this feature, some asynchronous control timeout scenarios can be realized:

func main() {
    ch := make(chan struct{}, 1)
    go func() {
        fmt.Println("do something...")
        time.Sleep(4*time.Second)
        ch<- struct{}{}
    }()
    
    select {
    case <-ch:
        fmt.Println("done")
    case <-time.After(3*time.Second):
        fmt.Println("timeout")
    }
}

Suppose here that there is a goroutine running a time-consuming task. Using select, there is a channel gets the data and exits. When goroutine does not complete the task in a limited time, the main goroutine will exit, which achieves the purpose of timeout.

output:

do something...
timeout

Timer.After is canceled, and the Channel sends a message at the same time. You can also close the channel and other notification methods.

Note that Channel should have a size to prevent blocking the goroutine and causing leakage.

Context

The second solution is to use context, go's context function is powerful;

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

    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())
    }

The same usage, context of Done() function returns a channel , the channel will be completed in the context of current work or cancellation takes effect.

timout context deadline exceeded

Through timeout.Err() can also know the reason why context

goroutine passes context

Another advantage of using context is that it can take advantage of its natural transmission characteristics in multiple goroutines, so that all goroutines that pass the context can receive cancellation notifications at the same time. This is very widely used in multi-go.

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 goroutine nested, you can context cancelled (of course, the premise is that context to be passed away)

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

Case in Gin

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

    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 for 10s to Gin the resources of 06179ef1cce372, and the principle of implementation is the same as the above example.

Summarize

Because the time to write go is not long, so I wrote a hand-training project: an interface stress test tool.


One of the very common requirements is to exit after N seconds from the stress test. This is just the application of related knowledge points. It is also a reference go

https://github.com/crossoverJie/ptg/blob/d0781fcb5551281cf6d90a86b70130149e1525a6/duration.go#L41


crossoverJie
5.4k 声望4k 粉丝