上下文Context(二)超时

Golang进阶·技术 · 2023-06-21

介绍

今天,我们讨论Go编程中context非常重要的一个东西:context超时。我们先简单举个现实生活中的一个场景:

想象一下您在一个游乐场,兴奋地想要坐上巨型过山车。但有一个问题:排队的队伍超级长,而且距离游乐场关闭只有一个小时的时间。你该怎么办?嗯,你可能会等一段时间,但不会等一个小时,对吧?如果你等了 30 分钟但仍未排在队伍的前面,你可以离开队列尝试其他的项目。这个例子就是我们所说的“超时”。

现在,想象一下你还在排队,突然下起了大雨。游乐场决定关闭过山车。你肯定不会还坚持去排队等待吧?你会马上离开或者去其他地方。这里就是我们所说的“取消”了。

在编程里面,我们经常也会遇到类似的情况。要求我们的程序执行可能需要很长时间或由于某种原因需要停止任务。那么这时候context包发挥作用就来了。它可以让我们能够优雅的去处理这些超时和取消

当然我们在上一篇《goroutine并发控制与通信》文章中也讲到了用context去实现多线程的并发控制与通信,这里我们更详细的去介绍context的使用方法。

工作原理

我们可以分下面几步去模拟上面的场景:

  1. 首先创建一个上下文,这里和上面场景的去排队过山车类似:

    ctx := context.Background()
  2. 然后我们在上下文中设置超时。就像决定放弃并尝试其他游乐设施之前要在队列中等待多长时间:

    // 这里设置等待10秒等待时间
    ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Second*10)
    defer cancel()
    *注意:完成后不要忘记调用 cancel,否则可能会资源的泄露
  3. 检查是否超时,使用上下文来检查我们是否等待了太长时间并且停止我们的下面的业务处理,这里有点像排队的是时候我们去看时间一样:

    select { 
    case <-time.After(time.Second * 15 ): // 当前业务处理需要 15 秒
        fmt.Println( "完成任务" ) 
    case <-ctxWithTimeout.Done(): 
        fmt.Println( "等得太久了吧!") // 这里表示只等了 10 秒
    }

    取消上下文,如果我们由于某些原因需要停止业务处理,我们可以取消上下文。就像听到过山车因下雨要立马关闭的通知一样。

    // 这里直接调用我们上面创建超时上下文的时候得到的取消函数
    cancel()

到此整个流程就到这里了,下面我们用具体的一个实例来演示一下。

实例

这里就以一个http请求为例:

假设请求可能需要很长时间才能处理,然后我们可以通过对请求超时时间设置最大限制,下面设置 2s 超时。

func hello(w http.ResponseWriter, req *http.Request) {
        // 2s 过期时间
    ctx, cancel := context.WithTimeout(req.Context(), 2*time.Second)
    fmt.Println("server: hello handler started")
    defer func() {
        cancel()
        fmt.Println("server: hello handler ended")
    }()
    select {
    case <-time.After(3 * time.Second):
        fmt.Fprintf(w, "hello\n") // 需要 3s 去处理业务逻辑
    case <-ctx.Done():
        err := ctx.Err()
        fmt.Println("server:", err)
        internalError := http.StatusInternalServerError
        http.Error(w, err.Error(), internalError)
    }
}

func main() {
    http.HandleFunc("/hello", hello)
    http.ListenAndServe(":8090", nil)
}

最后由于 2s 后请求就会超时,上下文被取消,我们在请求结果中就会得不到 hello 的结果,最终会是以下结果:

server: hello handler started
server: context deadline exceeded
server: hello handler ended

最后留一端代码,你们觉得打印内容是什么呢🤭?

func expensiveCalculation(resultChan chan<- int) {
    time.Sleep(5 * time.Second)
    resultChan <- 100
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    resultChan := make(chan int)
    go expensiveCalculation(resultChan)
    select {
    case res := <-resultChan:
        fmt.Printf("result: %d\n", res)
    case <-ctx.Done():
        fmt.Printf("result: %d\n", <-resultChan)
        fmt.Println("ctx 取消")
    }
}
并发编程 Context Golang进阶
Theme Jasmine by Kent Liao