Go语言怎么实现请求超时处理

寻技术 Go编程 2023年07月12日 101

这篇文章主要介绍“Go语言怎么实现请求超时处理”,在日常操作中,相信很多人在Go语言怎么实现请求超时处理问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Go语言怎么实现请求超时处理”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

    1. 简介

    但是在本文中,暂未展示在哪些场景下,

    timerCtx
    实现超时控制相对于
    timer
    实现的优点,或者在哪些场景下,
    timer
    相对于
    timerCtx
    在哪些场景下使用更为合适,后续将会再进行描述。

    2. 问题引入

    当使用Go语言进行网络请求时,程序可能会因为请求处理时间过长而被卡住,无法继续执行后续代码。这种情况会导致程序性能下降,用户体验变差,甚至会导致系统崩溃。特别是在高并发场景下,这种问题更加突出。

    举个例子,假设我们需要从一个远程服务获取一些数据,我们可以使用Go标准库中的http包进行网络请求。代码可能类似于以下示例:

    func makeRequest(url string) (string, error) {
        // 创建 http.Client 客户端实例
        client := &http.Client{}
        // 创建请求
        req, err := http.NewRequest("GET", url, nil)
        if err != nil {
            return "", err
        }
        // 执行请求
        resp, err := client.Do(req)
        if err != nil {
            return "", err
        }
        // 读取响应内容
        defer resp.Body.Close()
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            return "", err
        }
        return string(body), nil
    }
    func main() {
        url := "https://baidu.com"
        result, err := makeRequest(url)
        if err != nil {
            return
        }
    }

    这里定义了一个

    makeRequest
    函数,该函数使用
    http.Client
    客户端发送
    HTTP
    请求并返回响应体。

    但是,如果请求响应时间过长,程序就会一直等待直到请求超时或者响应返回。如果是单个请求的情况下,这种等待可能不会对系统产生太大的影响。但是在高并发场景下,这种情况可能会导致系统性能大幅下降。

    因此,我们需要一种方法来对请求进行超时处理,确保程序能够及时响应其他请求,而不是一直等待。

    3. timer的实现方案

    3.1 timer的基本介绍

    Timer
    可以通过
    time.NewTimer()
    time.AfterFunc()
    函数创建。
    NewTimer()
    函数创建一个
    Timer
    对象,该对象在指定的时间间隔后向一个通道发送一个当前时间。
    AfterFunc()
    函数则会在指定的时间间隔后执行一个函数。

    通过

    timer
    ,可以实现许多常见的任务,比如定期执行某个操作、超时控制、任务调度等。同时,在Go语言中,
    timer
    还可以方便地取消或重置,能够更加灵活地控制程序的运行。

    所以,这里我们可以使用

    timer
    实现请求的超时控制,下面我们来看使用
    timer
    来实现超时控制的具体步骤。

    3.2 timer实现超时控制

    如果需要使用

    timer
    实现请求的超时控制,可以通过以下步骤来实现请求的超时处理,具体如下:
    • 创建一个

      timer
      对象。可以使用
      time.NewTimer()
      函数创建一个新的timer对象
    • 启动一个goroutine来执行具体的业务逻辑

    • select
      语句中处理超时事件。在
      select
      语句中,使用一个case来处理
      timer
      的超时事件
    • 在需要控制超时的地方使用上述逻辑

    下面是一个示例代码,演示了如何使用timer实现超时控制:

    package main
    import (
        "fmt"
        "time"
    )
    func main() {
        // 1. 创建一个timer对象,等待5秒钟
        timeout := time.NewTimer(5 * time.Second)
        ch := make(chan string, 1)
        go func() {
           // 2. 这里我们简单模拟一个需要执行10秒的操作
           time.Sleep(10 * time.Second)
           ch <- "hello world"
        }()
        // 3. 在select语句中处理超时事件 或者请求正常返回
        select {
        case <-timeout.C:
            // 执行任务超时处理
           fmt.Println("操作超时")
           return
        case result := <-ch:
            // 执行正常业务流程
           fmt.Println(result)
        }
        // 停止timer
        if !timeout.Stop() {
           <-timeout.C
        }
        // 操作执行完成
        fmt.Println("操作执行完成")
    }

    这里在主协程处通过

    NewTimer
    创建一个定时器,然后启动一个协程对任务进行处理,当处理完成后,通过
    channel
    告知其他协程。

    在主协程中,通过

    select
    语句,对定时器
    timer
    channel
    同时进行监听,当任务执行超时时,则执行超时逻辑;如果任务在超时前完成,则执行正常处理流程。

    通过这种方式,实现了请求的超时处理。

    3.3 对问题的解决

    下面展示使用

    timer
    来实现对请求的超时处理,从而避免程序长期处于等待状态,造成系统性能大幅下降。
    func makeRequest(url string) (string, error) {
          // 具体的业务逻辑
    }
    func main() {
        url := "https://baidu.com"
        // 设置超时时间为5秒
        timeout := 5 * time.Second
        // 创建一个计时器,等待超时
        timer := time.NewTimer(timeout)
        // 创建一个 channel,用于接收请求的结果
        ch := make(chan string, 1)
        // 启动协程执行请求
        go func() {
            result, err := makeRequest(url)
            if err != nil {
                ch <- fmt.Sprintf("Error: %s", err.Error())
                return
            }
            ch <- result
        }()
        // 等待超时或者请求结果返回
        select {
        case result := <-ch:
            fmt.Println(result)
        case <-timer.C:
            fmt.Println("Request timed out")
        }
        // 请求完成后,停止定时器
        if !timer.Stop() {
            <-timer.C
        }
    }

    在这个示例中,我们使用

    time
    包创建一个计时器,等待超时。同时,我们还创建了一个 channel,用于接收请求的结果。然后我们启动一个协程执行请求,一旦请求返回,就会将结果发送到 channel 中。在主协程中,我们使用
    select
    语句等待超时或者请求结果返回。如果请求在超时之前返回,就会从 channel 中接收到结果并打印出来。如果请求超时,就会打印出相应的错误信息。

    从而实现了避免了处理某些场景请求时,避免系统进入长时间等待的问题的出现。

    4.timetCtx的实现方案

    虽然,

    timer
    select
    实现超时控制的逻辑并不复杂,但是在某些场景下,使用
    timerCtx
    来实现超时控制,相对来说是更为简单的,而且现有开源框架基本上也是通过该方式来实现的。所以接下来,我们来对
    timerCtx
    进行基本介绍,同时使用
    timerCtx
    来实现超时控制。

    4.1 timerCtx的基本介绍

    timerCtx
    是一种在Go语言中使用
    Context
    Timer
    结合实现超时控制的方式。它是一个自定义的结构体类型,用于封装定时器和取消函数,并提供一种方便的方式来取消
    goroutine
    的执行,从而避免出现
    goroutine
    泄露等问题。

    4.2 timerCtx的基本使用方式

    当使用

    timetCtx
    实现超时控制,通常需要以下几个步骤:
    • 调用

      context.WithTimeout()
      方法,创建一个超时控制的子上下文。
    • 启动一个协程来执行任务。

    • 在主协程中,通过

      select
      语句调用
      Done()
      方法来判断是否超时。如果
      Done()
      方法返回的
      channel
      被关闭,则意味着已经超时,需要及时停止当前任务并返回。
    • 在函数返回时,调用取消函数

      cancel()
      ,释放占用的资源。

    下面是一个示例代码,演示了如何使用

    timerCtx
    实现超时控制:
    package main
    import (
        "context"
        "fmt"
        "time"
    )
    func main() {
        // 创建一个timerCtx,设置超时时间为3秒
        ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
        // 调用cancel函数,释放占用的资源  
        defer cancel()
        // 开启一个协程执行任务
        ch := make(chan string, 1)
        go func() {
            // 模拟任务执行,休眠5秒
            time.Sleep(5 * time.Second)
            ch <- "hello world"
        }()
        // 在主协程中等待timerCtx超时或任务完成
        select {
        case <-ctx.Done():
            fmt.Println("timeout")
        case result := <-ch:
            fmt.Println(result)
        }
    }

    这里在主协程处通过

    context.WithTimeout
    创建一个
    timerCtx
    ,然后启动一个协程对任务进行处理,当处理完成后,通过
    channel
    告知其他协程。

    其次,对于

    timerCtx
    来说,调用
    Done
    方法将会返回一个
    channal
    ,当超时后,该
    channel
    将会自动被关闭,此时通过
    select
    ,将能够从该处于
    close
    状态的
    channel
    中接收到数据。

    因此,在主协程中,通过

    select
    语句,对这两个
    channel
    同时进行监听,当任务执行超时时,则执行超时逻辑;如果任务在超时前完成,则执行正常处理流程。通过这种方式,实现了请求的超时处理。

    4.3 对问题的解决

    下面使用

    context.WithTimeout
    select
    来实现请求的超时处理,通过这种方式,避免程序长期处于等待状态,具体代码实现如:
    // 执行具体的业务逻辑
    func makeRequest(ctx context.Context, url string) (string, error) {}
    func main() {
        url := "https://baidu.com"
        // 创建一个不带超时的context
        ctx := context.Background()
        // 1. 创建一个带超时的timerCtx
        timeout := 5 * time.Second
        timerCtx, cancel := context.WithTimeout(ctx, timeout)
        //5. 在函数返回时,调用取消函数 cancel(),释放占用的资源。
        defer cancel()
        // 创建一个 channel,用于接收请求的结果
        ch := make(chan string, 1)
        // 2. 将子上下文传递给需要进行超时控制的函数, 启动协程执行请求
        go func() {
           result, err := makeRequest(ctx,url)
            if err != nil {
                ch <- fmt.Sprintf("Error: %s", err.Error())
                return
            }
            ch <- result
        }()
        // 函数可以通过调用 context.Context 对象的 Done() 方法来判断是否超时。
        // 如果 Done() 方法返回的 channel 被关闭,则意味着已经超时,需要及时停止当前任务并返回。
        select {
        case result := <-ch:
            fmt.Println(result)
        case <-timerCtx.Done():
            fmt.Println("Request timed out")
        }
    }

    在这个例子中,我们使用

    context.WithTimeout
    创建一个带有超时的 context 对象,设置超时时间为 5秒钟。
    handleRequest
    来执行对应的任务,将
    timeCtx
    传递给
    handleRequest
    ,如果没有在对应时间内正常返回,此时任务会直接返回,不会无限期执行下去。

    在任务执行过程中,通过

    select
    不断检查
    ctx.Done()
    方法的返回值,如果超时时间到了,
    ctx.Done()
    的结果将变为一个非
    nil
    的值,这时我们就可以在
    select
    语句中执行超时处理的逻辑。

    最后,在任务返回后,调用取消函数

    cancel()
    ,释放占用的资源。

    从上面

    timer
    实现超时控制,或者是使用
    timerCtx
    的实现来看,其实二者区别并不大,但是事实上,现在任务的超时控制,基本上都是使用
    timerCtx
    实现的,并非使用
    timer
    来实现的,后续将会对其进行说明。
    关闭

    用微信“扫一扫”