Content Table

Go 实现超时处理

Go 通常可以使用以下 3 种方式实现超时:

  • Timeout Context 实现超时
  • Cancel Context 实现超时
  • 手动操作 channel 实现超时

推荐使用 Cancel Context 的方式实现超时功能,因为区分是正常结束还是超时结束比较容易。

Timeout Context 实现超时

TimeoutContext 的特点:

  • 超时会自动往 Done() 返回的 channel 里写入数据。
  • 手动调用 CancelFunc 也会往 Done() 返回的 channel 里写入数据。
  • 需要对 Context.Err() 进行判断区分是正常结束还是自动超时结束。

案例代码:

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
package main

import (
"context"
"fmt"
"math/rand"
"time"
)

func main() {
rand.Seed(time.Now().UTC().UnixNano())
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)

// 异步请求
go func(cancel func()) {
defer cancel()

fmt.Println("Query Start...")

// 模拟耗时请求,耗时随机 1 到 6 秒
d := 1 + rand.Intn(6) // 1..6
fmt.Printf("Query use %dS\n", d)
time.Sleep(time.Second * time.Duration(d))
fmt.Println("Query Finished!")
}(cancel)

// [*] 等待超时结束或者请求提前结束
<-ctx.Done()

// 1. 请求结束, Error: context canceled
// 2. 请求结束, Error: context deadline exceeded
fmt.Printf("请求结束, Error: %v\n", ctx.Err())
}

正常输出:

1
2
3
4
Query Start...
Query use 2S
Query Finished!
请求结束, Error: context canceled

超时输出:

1
2
3
Query Start...
Query use 4S
请求结束, Error: context deadline exceeded

Cancel Context 实现超时

Cancel Context 的特点:

  • 手动调用 CancelFunc 会往 Done() 返回的 channel 里写入数据。
  • 使用 time.After() 实现超时。
  • 区分是正常结束还是超时结束比较容易。
  • 与 Timeout Context 相比,需要使用 select 进行超时判的。

案例代码:

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
35
36
package main

import (
"context"
"fmt"
"math/rand"
"time"
)

func main() {
rand.Seed(time.Now().UTC().UnixNano())
ctx, cancel := context.WithCancel(context.Background())

// 异步请求
go func(cancel func()) {
defer cancel()

fmt.Println("Query Start...")

// 模拟耗时请求,耗时随机 1 到 6 秒
d := 1 + rand.Intn(6) // 1..6
fmt.Printf("Query use %dS\n", d)
time.Sleep(time.Second * time.Duration(d))
fmt.Println("Query Finished!")
}(cancel)

// [*] 等待超时结束或者请求提前结束
select {
case <-ctx.Done():
fmt.Printf("请求正常结束, Error: %v\n", ctx.Err())
case <-time.After(time.Second * 3):
fmt.Println("超时")
}

fmt.Printf("Error: %v\n", ctx.Err())
}

正常输出:

1
2
3
4
5
Query Start...
Query use 2S
Query Finished!
请求正常结束, Error: context canceled
Error: context canceled

超时输出:

1
2
3
4
Query Start...
Query use 5S
超时
Error: <nil>

手动操作 channel 实现超时

手动操作 channel 实现超时的特点:

  • 与 Cancel Context 实现的超时很相似,也是使用 select 来进行超时判的。
  • 但是在并发场景的时候,手动 channel 实现超时就比较麻烦,Context 可以很好的简化代码。

案例代码:

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
package main

import (
"fmt"
"math/rand"
"time"
)

func main() {
rand.Seed(time.Now().UTC().UnixNano())
doneC := make(chan struct{})

// 异步请求
go func(quit chan<- struct{}) {
defer close(doneC)

fmt.Println("Query Start...")

// 模拟耗时请求,耗时随机 1 到 6 秒
d := 1 + rand.Intn(6) // 1..6
fmt.Printf("Query use %dS\n", d)
time.Sleep(time.Second * time.Duration(d))
fmt.Println("Query Finished!")
}(doneC)

// [*] 等待超时结束或者请求提前结束
select {
case <-doneC:
fmt.Println("请求正常结束")
case <-time.After(time.Second * 3):
fmt.Println("超时")
}
}

正常输出:

1
2
3
4
Query Start...
Query use 1S
Query Finished!
请求正常结束

超时输出:

1
2
3
Query Start...
Query use 6S
超时