关于go:为什么需要超时控制

2次阅读

共计 5052 个字符,预计需要花费 13 分钟才能阅读完成。

# 1. 简介

本文将介绍为什么须要超时管制,而后具体介绍 Go 语言中实现超时管制的办法。其中,咱们将探讨 time 包和 context 包实现超时管制的具体形式,并阐明两者的实用场景,以便在程序中以更适合的形式来实现超时管制,进步程序的稳定性和可靠性。

2. 为什么须要超时管制

超时管制能够帮忙咱们防止程序无限期地期待某个操作实现或者避免程序因为某些起因导致资源透露。具体来说,超时管制有以下几个长处:

  1. 防止无限期期待:如果某个操作须要期待一段时间,但如果超过了这个工夫还没有实现,那么程序可能就须要进行期待并采取其余措施,比方返回谬误或者勾销操作。超时解决能够让程序防止无限期期待,从而进步程序的健壮性和可靠性。
  2. 避免资源透露:如果程序某个操作始终处于期待状态,那么可能会导致资源被始终占用,从而造成资源透露。超时管制能够让程序在肯定工夫内实现操作,如果超时则开释资源,从而防止资源透露。
  3. 进步程序响应速度:有些操作可能须要很长时间能力实现,这可能会导致程序在期待过程中变得不响应。超时管制能够让程序在肯定工夫内实现操作,如果超时则采取其余措施,从而进步程序的响应速度。

基于以上几点起因,咱们能够看进去,在某些场景下,超时管制在程序执行过程中必不可少。那么,在 Go 语言中,有哪些形式能够实现超时管制呢?

3. 超时管制的办法

 3.1 time 包实现超时管制    

time包提供了多种形式来实现超时管制,包含 time.After 函数、time.NewTimer函数以及 time.AfterFunc 函数,应用它们能够实现超时管制,上面以 time.NewTimer 函数为例,阐明如何应用其 time 包实现超时管制。代码示例如下:

// 创立一个定时器
timer := time.NewTimer(5 * time.Second)
defer timer.Stop()

// 应用一个 channel 来监听工作是否已实现
ch := make(chan string, 1)     
go func() {         
// 模仿工作执行,休眠 5 秒         
    time.Sleep(2* time.Second)         
    ch <- "hello world"     
}()

// 通过 select 语句来期待后果, 工作失常返回
select {
case <-ch:
    fmt.Println("工作失常实现")
  // ch 曾经接管到值,走失常解决逻辑
case <-timer.C:
    fmt.Println("已超时")
  // 超时,走超时逻辑
}

在这里例子中,咱们应用 time.NewTimer 办法创立一个定时器,超时工夫为 2 秒钟。而后在 select 语句中应用来期待后果,哪个先返回就应用哪个。

如果操作在 2 秒钟内实现,那么工作失常实现;如果操作超过 2 秒钟仍未实现,此时 select 语句中 <-timer.C 将接管到值,走超时解决逻辑。

3.2 context 实现超时管制

Context 接口是 Go 语言规范库中提供的一个上下文(Context)管理机制。它容许在程序的不同局部之间传递上下文信息,并且能够通过它实现超时管制、勾销操作以及截断操作等性能。其中,Context接口存在一个 timerCtx 的实现,其能够设定一个超时工夫,在达到超时工夫后,timerCtx对象的 done channel 将会被敞开。

当须要判断是否超时时,只须要调用 context 对象的 Done 办法,其会返回 timerCtx 对象中的 done channel,如果有数据返回,则阐明曾经超时。基于此,咱们便能够实现超时管制。代码示例如下:

// 创立一个 timerCtx,设置超时工夫为 3 秒     
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)     
// 调用 cancel 函数, 开释占用的资源  
defer cancel()

// 应用一个 channel 来监听工作是否已实现
ch := make(chan string, 1)     
go func() {         
// 模仿工作执行,休眠 5 秒         
    time.Sleep(2* time.Second)         
    ch <- "hello world"     
}()

// 通过 select 语句来期待后果, 工作失常返回
select {case <-ctx.Done():
        fmt.Println("timeout")
    case result := <-ch:
        fmt.Println(result)
}

这里通过 context.WithTimeout 创立一个timerCtx,设定好超时工夫,超时工夫为 3s。而后启动一个协程来执行具体的业务逻辑。

之后通过 select 语句,对 timerCtx 和业务执行后果同时进行监听,当工作解决超时时,则执行超时逻辑;如果工作在超时前实现,则执行失常解决流程。通过这种形式,实现了申请的超时解决。

4. 实用场景剖析

从上文能够看出,timetimerCtx都能够用于实现超时管制,然而事实上两者的实用场景其实是不太雷同的。在某些场景下, 超时管制并不适宜应用 time 来实现,而是应用 timerCtx 来实现更为适合。而在某些场景下,其实两种实现形式均可。

上面我简略介绍几种常见的场景,而后对其来进行剖析,从而可能在适合的场景下应用失当得实现。

4.1 简略超时管制

举个例子,假如咱们须要从一个近程服务获取一些数据,咱们能够应用 Go 规范库中的 http 包进行网络申请,大略申请函数如下:

func makeRequest(url string) (string, error) {// 申请数据}

此时为了防止申请响应工夫过长,导致程序长时间处于期待状态,此时咱们须要对这个函数实现超时解决,确保程序可能及时响应其余申请,而不是始终期待。

为了实现这个目标,此时能够应用 time 包或者 timerCtx 来实现超时管制。在 makeRequest 函数中实现超时管制,这里代码展现与 第三点超时管制的办法 中的代码示例大体雷同,这里不再赘述。而且,查看下面代码示例,咱们也能够看进去 timer 或者 timerCtx 在这个场景下,区别并不大,此时是能够互相替换的。

因而,对于这种管制某个函数的执行工夫的场景,是能够任意筛选 time 或者 timerCtx 其中一个来实现的。

4.2 可选超时管制

这里咱们实现一个办法,用于建设网络连接,用户调用该办法时,传入待建设连贯的地址列表,而后该办法通过遍历传入的地址列表,并针对每一个地址进行连贯尝试,直到连贯胜利或者所有地址都尝试实现。函数定义如下:

func dialSerial(ras addrList) (Conn, error){// 执行建设网络连接的逻辑}

基于此,在这个函数的根底上,实现一个可选的超时管制的性能。如果用户调用该办法时,有指定超时工夫的话,此时便进行超时管制;如果未指定超时工夫的话,此时便无需执行超时管制。这里别离应用 time 包以及 context 实现。

首先对于 time 包实现可选的超时管制,能够通过函数参数传递定时器来实现可选的超时管制。具体地说,能够将定时器作为一个 time.Timer 类型的参数传递给函数,而后在函数中应用 select 监听 time.Timer 是超时;如果没有传递定时器实例,则默认不进行超时管制,代码实现如下所示:

func dialSerial(timeout time.Timer, ras addrList) (Conn, error){
   // 执行建设网络连接的逻辑,对每个地址尝试建设连贯时,先查看是否超时
   for i, ra := range ras {
          // 通过这里来进行超时管制, 首先先判断是否传入定时器实例
          if timeout != nil {
              select {
              // 监听是否超时
              case <-timeout.C:
                  return nil, errors.New("timeout")
              default:
              }
          }
         // 执行后续建设网络连接的逻辑          
   }
}

接着则是应用 timerCtx 来实现超时管制的实现,能够通过函数传递一个 context.Context 接口的参数来实现超时管制。

具体来说,用户能够传递一个 context.Context 接口的实现,如果有指定超时工夫,则传入一个 timerCtx 的实现;如果无需超时管制,此时能够传入 context.Background,其永远不会超时。而后函数中通过调用Done 办法来判断是否超时,从而实现超时管制。代码实现如下:

func dialSerial(ctx context.Context, ras addrList) (Conn, error){
   // 执行建设网络连接的逻辑,对每个地址尝试建设连贯时,先查看是否超时
   for i, ra := range ras {
       select {case <-ctx.Done():
          return nil, &OpError{Op: "dial", Net: sd.network, Source: sd.LocalAddr, Addr: ra, Err: mapErr(ctx.Err())}
       default: 
       }
       // 执行建设网络连接的逻辑
   }
}

查看上述代码中,dialSerial函数实现可选超时管制,看起来只是传入参数不同,一个是传入定时器 time.Timer 实例,一个是传入 context.Context 接口实例而已,然而实际上不仅仅如此。

首先是代码的可读性上来看,传入 time.Timer 实例来实现超时管制,并非 Go 中常见的实现形式,用户不好了解;而对于 context.Context 接口来说,其被宽泛应用,如果要实现超时管制,用户只须要传入一个 timerCtx 实例即可,用户应用起来没有额定的心智累赘,代码可读性更强。

其次是对于整个 Go 语言的生态来说,context.Context接口在 Go 语言规范库中失去宽泛应用,而且广泛超时管制都是应用 timerCtx 来实现的,如果此时传入一个 time.Timer 实例,实际上是与整个 Go 语言的超时管制的心心相印的。以下面 dialSerial 办法为例,其建设网络连接是须要调用底层函数来帮助实现的,如:

func (fd *netFD) connect(ctx context.Context, la, ra syscall.Sockaddr) (rsa syscall.Sockaddr, ret error) {
    // 执行建设连贯的逻辑
    switch err := connectFunc(fd.pfd.Sysfd, ra); err {
    // 未报错, 此时查看是否超时
    case nil, syscall.EISCONN:
       select {case <-ctx.Done():
           // 如果曾经超时, 此时返回超时谬误
          return nil, mapErr(ctx.Err())
       default:
       }
     }
}

而且刚好,该函数也是实现了可选的超时管制,而且是通过 timerCtx 来实现的,如果此时传入的 timerCtx 曾经超时,此时函数会间接返回一个超时谬误。

如果下面 dialSerial 的超时管制是通过 context.Context 的接口实例来实现的话,此时调用函数时,间接将内部的 Context 实例作为参数传入 connect 函数,外层调用也无需再查看函数是否超时,代码的可复用性更高。

绝对的,如果 dialSerial 的超时管制是通过传入定时器实现的,此时便无奈很好利用 connect 办法曾经实现的超时查看的机制。

因而,综上所述,应用 context.Context 接口作为可选的超时控制参数,相比于应用 time.Timer,更加适宜同时也更加高效,与整个 Go 语言的实现也可能更好得进行交融在一起。

4.3 总结

ContextTime 都是 Go 语言中实现超时管制的办法,它们各有优缺点,不能说哪一种实现更好,要依据具体的场景来抉择应用哪种办法。

在一些简略的场景下,应用 Time 包实现超时管制可能更加不便,因为它的 API 更加简略,只须要应用 time.After() 函数即可实现超时管制。

然而,如果波及到在多个函数,或者是须要多个 goroutine 之间传递的话,此时应用 Context 来实现超时管制可能更加适宜。

5. 总结

本文介绍了须要超时管制的起因,次要是防止无限期期待,避免资源透露和进步程序响应速度这几点内容。

接着咱们介绍了 Go 语言中实现超时管制的办法,包含应用 time 实现超时管制以及应用 context 实现超时管制,并给出了简略的代码示例。

在接下来,咱们便这两种实现的实用场景进行剖析,明确了在哪些场景下,适宜应用 time 实现超时管制,以及在哪些场景下,应用 timerCtx 来实现更为高效。

基于此,实现了为什么须要超时管制的介绍,心愿可能让大家在遇到须要超时管制的场景下,更好得去进行实现。

正文完
 0