Golang context包用法详解(附带实例)
上下文(Context)是一种很泛化的概念,可以理解为前因后果。同样的一句话,在不同的上下文环境中,表达的意思很有可能是不一样的。在编程语言中,上下文是指在 API 或者方法之间传递的除业务参数以外的信息。
有时,我们在执行某项任务时会设定一些条件,比如如果遇到特定情况或者出错时就必须取消这项任务。如果是简单的取消,我们可以发出信号通知,但对于复杂的场景,如果要取消的任务不是单个叶子节点上的,而是由一个协程衍生出来的有多个子协程的任务,并且协程相互之间满足一定的约束关系,那这时取消任务就是一件麻烦事儿,如下图所示。

图 1 关联任务的取消
在 Go 语言中,开发服务端程序时通常会为每个请求创建一个协程,以便并发地处理任务。也有可能会创建子协程,用于访问数据库或者完成 RPC 服务调用。当一个请求超时或者被终止时,需要退出所有衍生的协程,并释放资源。
在 Go 语言底层的设计中,创建协程时并不会返回协程 ID。如果想退出父协程,就需要把以父协程为根的所有子协程任务都取消掉,但手动取消不是一件容易的事。因此,我们需要通过一种优雅的机制来通知衍生协程请求已被取消。
服务器不可能始终只处理某一个请求或某一项任务。所以,开发者必须提醒自己注意时间限制。可以使用 context 包来设置一个超时时间,当超过这个时间时,这个操作就会被取消。
有时,我们需要在一个请求的生命周期内,跨函数、协程甚至进程传递一些与请求相关的值,对此也可以使用 context 包来实现。如果我们提供了一个服务端程序,希望当用户取消一个请求时,所有与这个请求相关的操作(包括后续派生的其他请求)都能被取消,这仍然可以使用 context 包来实现。当我们需要对请求进行跟踪,或者要在请求的生命周期内记录一些日志信息时,还是可以使用 context 包来实现。
在 Go 语言的生态中,有很多应用框架都使用了 context 包,包括 database/sql、os/exec、net、net/http 等。因此,熟练掌握 context 包,对于开发和维护 Go 语言应用来说至关重要。
context 包中重要的接口和函数如下表所示:
注意,context.WithDeadline 和 context.WithTimeout 函数都是用于创建新的 Context,这两个新的 Context 都会在指定的时间点被取消。它们的区别主要在于如何指定取消的时间点:
context 包内部实现了从主协程上下文开始遍历它所派生的所有子协程的上下文的算法,我们不需要特别关注这个算法,只需要掌握 context 包的使用方法即可,它的使用流程如下。
1) 使用 context 包提供的 context.Background 函数创建一个初始的 Context,并以此作为所有其他 Context 的根节点。这个函数返回一个空的 Context,不包含任何元数据、超时时间或者取消信号。
2) 通过 context.WithXxx(parentContext) 函数创建相应的 Context。除了 WithValue 方法生成的是不可撤销的上下文,另外三个方法(WithCancel、WithTimeout、WithDeadline)都会生成可撤销的上下文。
3) 使用 Context的 Value(key interface{}) interface{} 方法从 Context 中获取元数据。如果 key 不存在,则返回 nil。
4) 使用 cancel 取消 Context。如果当前 Context 被取消,则基于它的子 Context 都会被取消。
5) 使用 <-ctx.Done 接收取消通知。可以通过 Done 方法检查 Context 是否已经取消或者超时。这个方法返回一个通道,如果 Context 已经取消或者超时,则通道会被关闭。
有两个生成根 Context 的方法,分别是 context.Background 和 context.TODO,它们没有本质区别,都会生成一个空的上下文 new(emptyCtx)。context.Background 常用作最顶层的父 Context,它不能被取消。
context.Context 接口的定义如下:
context.Context 接口提供的方法如下表所示:
注意,表格中的 Context 不是特指某个 context.Context 类型的对象,而是泛指任何实现了该接口的对象。
使用 ctx 对象时,可以通过调用 ctx.Value(key) 方法来获取与 key 相关联的值。如果 key 不存在,该方法返回 nil。需要注意的是,ctx.Value(key) 方法只能获取与 key 相关联的值,不能修改或删除这个值。如果需要修改或删除这个值,需要创建一个新的 Context 对象。
Context 对象支持以嵌套的方式设置键值对。示例代码如下:
使用 ctx 对象时,可以通过调用 cancel 函数来取消与 ctx 对象相关的操作。当 ctx 对象被取消时,与 ctx 对象相关的所有操作都应该尽快终止。此时,调用 ctx.Done 方法会返回一个 chan struct{} 类型的通道,这个通道会立即关闭。如果需要获取与取消原因相关的信息,可以调用 ctx.Err 方法,它会返回一个 context.Canceled 错误。
需要注意的是,调用 cancel 函数时,只会发送信号量,并不会真正地取消协程。要真正地取消协程,必须先在协程中用 select 的 case 分支捕获到信号 <-ctx.Done。
下面的例子演示了在协程 g1 中调用协程 g2 的方法,根据 context 包提供的功能可知,在取消协程 g1 后,会自动取消与之关联的协程 g2。
需要注意的是,在取消 ctx 对象时,可能会触发一些清理操作,例如关闭文件、释放锁等。因此,需要在相关的操作中正确地处理取消信号,避免出现资源泄露或其他问题。
context.WithTimeout 函数一般需要与 select 配合使用,其使用方式与 context.WithCancel 函数类似,示例代码如下:
当 ctx.Done 方法返回的通道关闭时,可以根据 ctx.Err 方法返回的错误信息来判断是哪种情况发生了。需要注意的是,这里要使用值语义来操作 Context,因为 Context 在调用函数的过程中可能会发生变化。ctx.Done 是一个返回通道的方法,只要一进入 select 代码段,就开始倒计时。
context.WithTimeout 函数常用于对超时敏感的操作,例如网络请求、I/O 操作等。通过设置合适的超时时间,可以避免等待时间过长而对系统性能产生负面影响。
使用 ctx 对象时,可以通过 ctx.Done 方法获取一个 chan struct{} 类型的通道,这个通道会在以下任意一件事件发生时关闭:
context.WithTimeout 函数中的超时时间是相对时间,即从当前时间开始计算,而 context.WithDeadline 函数中的截止日期是绝对时间。它们的原理差不多,要做的都是等待某个协程在规定时间范围内完成工作。笔者更喜欢用前一种方式来实现超时。
context.WithDeadline 函数常用于对超时敏感的操作,例如网络请求、I/O操作等。在这些操作中,如果等待的时间过长,可能会对系统性能产生负面影响。
以上就是 context 包中各种函数的说明,它们可以保存值,也可以把超时的任务取消。
在下面的示例中,我们编写了一个 HTTP 服务,如果处理一个客户端请求所花费的时间超过 3 秒,就会返回“执行超过3秒”的提示。
还可以进一步完善代码,当达到预设的超时时间时,先不取消,而是尝试重试操作。关键代码如下:
retryTimeout 函数使用上下文对象 ctx 来控制超时和取消操作,并使用定时器来实现超时机制。每次重试都会等待一段时间,以避免操作过于频繁。
1) 不要把 Context 作为结构体的成员,建议将其作为第一个参数传递给函数,通常命名为 ctx。这样做可以将上下文与函数调用明确地关联起来,以避免上下文被误用。
2) 即使方法允许,也不要传递 nil 值的 Context。因为 nil 值的 Context 不能传递与请求相关的元数据,也不能取消上下文。如果在代码中不小心使用了 nil 值的 Context,就会导致应用程序出现不可预料的错误或者异常。如果不确定应该使用哪个 Context,可以使用 context.TODO,它会返回一个空的、不可取消的 Context。这个 Context 可以在应用程序的开发阶段作为占位符或者提示使用,开发者可将其替换为具体的 Context。
3) 使用 context.Context 接口的 Value 相关方法时,建议只用其传递与请求相关的元数据,而不要用它来传递可选的参数。因为 Value 方法是基于链式调用的,每次调用都会返回一个新的 Context 对象,而这个新的 Context 对象会包含原有的值和新的键值对,这会导致 Context 对象不断增大,最终会影响应用程序的性能。另外,使用 Value 方法传递可选参数也不利于代码的维护和扩展。
4) 一个 Context 可以传递给多个协程,这样,在函数或方法内部就可以使用 Context 中的元数据来完成相关的操作。由于 Context 是并发安全的,多个协程可以同时访问同一个 Context 对象,因此不会产生竞争或者死锁。此外,因为 Context 支持超时和取消机制,所以可以在任意一个协程中取消请求,从而避免出现资源泄露和无用的计算等情况。
有时,我们在执行某项任务时会设定一些条件,比如如果遇到特定情况或者出错时就必须取消这项任务。如果是简单的取消,我们可以发出信号通知,但对于复杂的场景,如果要取消的任务不是单个叶子节点上的,而是由一个协程衍生出来的有多个子协程的任务,并且协程相互之间满足一定的约束关系,那这时取消任务就是一件麻烦事儿,如下图所示。

图 1 关联任务的取消
在 Go 语言中,开发服务端程序时通常会为每个请求创建一个协程,以便并发地处理任务。也有可能会创建子协程,用于访问数据库或者完成 RPC 服务调用。当一个请求超时或者被终止时,需要退出所有衍生的协程,并释放资源。
在 Go 语言底层的设计中,创建协程时并不会返回协程 ID。如果想退出父协程,就需要把以父协程为根的所有子协程任务都取消掉,但手动取消不是一件容易的事。因此,我们需要通过一种优雅的机制来通知衍生协程请求已被取消。
Go语言context包的使用场景
幸运的是,在 Go1.7 版本中,正式把与上下文有关的包 golang.org/x/net/context(简称 contex 包)纳入了标准库。利用 context 包能够方便地处理多个协程之间的信号传递和资源清理工作。服务器不可能始终只处理某一个请求或某一项任务。所以,开发者必须提醒自己注意时间限制。可以使用 context 包来设置一个超时时间,当超过这个时间时,这个操作就会被取消。
有时,我们需要在一个请求的生命周期内,跨函数、协程甚至进程传递一些与请求相关的值,对此也可以使用 context 包来实现。如果我们提供了一个服务端程序,希望当用户取消一个请求时,所有与这个请求相关的操作(包括后续派生的其他请求)都能被取消,这仍然可以使用 context 包来实现。当我们需要对请求进行跟踪,或者要在请求的生命周期内记录一些日志信息时,还是可以使用 context 包来实现。
在 Go 语言的生态中,有很多应用框架都使用了 context 包,包括 database/sql、os/exec、net、net/http 等。因此,熟练掌握 context 包,对于开发和维护 Go 语言应用来说至关重要。
Go语言context包的接口和函数
在 Go 语言中,context 包提供了处理多个协程间信号传递和资源清理的工具。特别是在处理超时、取消操作,以及跟踪请求的生命周期、传递请求作用域的数据时,context 包提供了一种简洁的处理方式。context 包中重要的接口和函数如下表所示:
名称 | 类型 | 作用 |
---|---|---|
context.Context | 接口 | 定义了四个方法:Done、Err、Deadline 和 Value,分别用于表示操作是否完成、获取错误信息、获取超时时间和获取关联数据 |
context.Background | 函数 | 返回一个空的 Context,这个 Context 不能被取消,且没有值和截止时间。它通常在主函数、初始化以及测试代码中使用,用作所有 Context 的根 |
context.TODO | 函数 | 与 context.Background 类似,返回一个空的 Context |
context.WithValue(parent Context, key, val) | 函数 | 生成不可撤销的 Context。可用于传递上下文的值,值以键值对的形式存储 |
context.WithCancel(parent Context) | 函数 | 生成可撤销的 Context,同时返回一个cancel函数。需要手动调用这个函数来撤销协程 |
context.WithTimeout(parent Context, timeout time.Duration) | 函数 | 生成可定时撤销的 Context(超时撤销),表示从现在开始到多久结束。与 WithDeadline 函数的用法类似,在 WithTimeout 函数内调用了 WithDeadline 函数 |
context.WithDeadline(parent Context, d time.Time) | 函数 | 生成可定时撤销的 Context(超时撤销),表示什么时间点结束。与WithCancel 函数一样,WithDeadline 函数返回的 cancel 函数需要手动调用,并且必须尽快调用,以便尽早释放资源,不要单纯地依赖截止时间 |
注意,context.WithDeadline 和 context.WithTimeout 函数都是用于创建新的 Context,这两个新的 Context 都会在指定的时间点被取消。它们的区别主要在于如何指定取消的时间点:
- context.WithDeadline 函数是在未来具体的某一时间点取消;
- context.WithTimeout 函数则是在当前时间之后的某一段时间内取消。
Go语言context包的使用流程
在实际编码中,每产生一个请求,都会创建一个协程去处理,但是这种协程往往会派生出许多额外的协程,以完成如连接数据库、RPC 请求等任务。这些派生的协程和主协程共享多种信息和状态,包括共享同一个请求生命周期、用户认证信息和令牌等。显然,主协程和派生的子协程之间形成了树结构。context 包用于实现一对多的协程协作,当某个请求超时或者被取消时,它可以帮助我们结束相关的所有协程。context 包内部实现了从主协程上下文开始遍历它所派生的所有子协程的上下文的算法,我们不需要特别关注这个算法,只需要掌握 context 包的使用方法即可,它的使用流程如下。
1) 使用 context 包提供的 context.Background 函数创建一个初始的 Context,并以此作为所有其他 Context 的根节点。这个函数返回一个空的 Context,不包含任何元数据、超时时间或者取消信号。
2) 通过 context.WithXxx(parentContext) 函数创建相应的 Context。除了 WithValue 方法生成的是不可撤销的上下文,另外三个方法(WithCancel、WithTimeout、WithDeadline)都会生成可撤销的上下文。
- 使用 context.WithValue(parent Context, key interface{}, val interface{}) 函数将元数据存储到 Context 中。这个函数会返回一个新的 Context,其中包含了新的元数据。注意,Context 是不可变的,因此不能修改已经存在的 Context 中的元数据。
- 使用 context.WithCancel(parent Context) 函数创建一个带有取消信号的 Context。这个函数会返回一个新的 Context 和一个取消函数,可以在需要取消请求时手动调用取消函数 cancel。
- 使用 context.WithTimeout(parent Context, timeout time.Duration) 函数创建一个带有超时时间的 Context。这个函数会返回一个新的 Context 和一个取消函数,到达超时时间时会自动调用取消函数。在到达超时时间之前,可以通过 Context 的 Done 方法检查是否已经取消了请求。
- 使用 context.WithDeadline(parent Context, timeout time.Duration) 函数创建一个具有特定截止时间的 Context(也算一种超时撤销)。这个函数会返回一个新的 Context 和一个取消函数,当到达设定的截止时间时会自动调用该取消函数。在到达超时时间之前,可以通过 Context 的 Done 方法检查是否已经取消了请求。
3) 使用 Context的 Value(key interface{}) interface{} 方法从 Context 中获取元数据。如果 key 不存在,则返回 nil。
4) 使用 cancel 取消 Context。如果当前 Context 被取消,则基于它的子 Context 都会被取消。
5) 使用 <-ctx.Done 接收取消通知。可以通过 Done 方法检查 Context 是否已经取消或者超时。这个方法返回一个通道,如果 Context 已经取消或者超时,则通道会被关闭。
有两个生成根 Context 的方法,分别是 context.Background 和 context.TODO,它们没有本质区别,都会生成一个空的上下文 new(emptyCtx)。context.Background 常用作最顶层的父 Context,它不能被取消。
Go语言context.Context接口
Go 语言中的 context 包主要是通过 context.Context 接口来工作的。这个接口定义了一些基本的方法,在处理请求的过程中用于传递超时、取消信号以及其他与请求相关的值。context.Context 接口的定义如下:
type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key interface{}) interface{} }
context.Context 接口提供的方法如下表所示:
方法名 | 作用 |
---|---|
Deadline | 返回 Context 的截止时间,以及截止时间是否可用的标志。如果未设置截止日期,则标志的值是 false。以后每次调用此对象的 Deadline 方法时,都会返回与第一次调用相同的结果 |
Done | 返回一个通道,该通道在 Context 被取消或者超时关闭。如果 Context 不能被取消,那么 Done 方法可能返回 nil。Done 方法的返回值用于接收取消事件,它与 select 语句配合使用。当 Done 方法被关闭时,可以通过 ctx.Err 获取错误信息 |
Err | 返回 Context 被取消的原因,如果 Context 没有被取消,则返回nil |
Value | 返回 Context 中携带的值,该值是键值对的形式。如果 key 不存在,则返回nil |
注意,表格中的 Context 不是特指某个 context.Context 类型的对象,而是泛指任何实现了该接口的对象。
Go语言生成Context的方法
前面在介绍 context 包时,讲解了可以生成 Context 的函数为 context.WithValue、context.WithCancel、context.WithTimeout 和 context.WithDeadline,接下来会详细介绍这些函数。1) context.WithValue函数
context.WithValue 函数用于创建一个带有键值对的 Context 对象,示例代码如下:ctx := context.WithValue(parentContext, key, value)其中,parentContext 是一个已有的 Context 对象,key 是一个任意类型的键值,用于标识对应的值,value 是一个任意类型的值,与 key 相关联。返回的 ctx 是一个新的 Context 对象,可用于带有键值对的操作。
使用 ctx 对象时,可以通过调用 ctx.Value(key) 方法来获取与 key 相关联的值。如果 key 不存在,该方法返回 nil。需要注意的是,ctx.Value(key) 方法只能获取与 key 相关联的值,不能修改或删除这个值。如果需要修改或删除这个值,需要创建一个新的 Context 对象。
Context 对象支持以嵌套的方式设置键值对。示例代码如下:
func Wakeup(ctx context.Context) { fmt.Println(ctx.Value("上文"))//根据键取值 fmt.Println("起床了") //用于模拟协程要做的真正业务 go Play(context.WithValue(ctx,"下文","要出去玩"))//设置新的值,key=下文,值=要出去玩 } func Play(ctx context.Context) { fmt.Println(ctx.Value("下文"))//根据键取值 } func main() { ctx:=context.WithValue(context.Background(),"上文","今天是周末") go Wakeup(ctx) time.Sleep(time.Second) }执行结果为:
今天是周末
起床了
要出去玩
2) context.WithCancel函数
context.WithCancel 函数用于生成一个带取消函数 cancel 的 Context 对象,示例代码如下:ctx, cancel := context.WithCancel(parentContext) defer cancel()其中,parentContext 是一个已有的 Context 对象,cancel 是一个函数,用于取消与 ctx 相关的操作。返回的 ctx 是一个新的 Context 对象,用于带有取消功能的操作。
使用 ctx 对象时,可以通过调用 cancel 函数来取消与 ctx 对象相关的操作。当 ctx 对象被取消时,与 ctx 对象相关的所有操作都应该尽快终止。此时,调用 ctx.Done 方法会返回一个 chan struct{} 类型的通道,这个通道会立即关闭。如果需要获取与取消原因相关的信息,可以调用 ctx.Err 方法,它会返回一个 context.Canceled 错误。
需要注意的是,调用 cancel 函数时,只会发送信号量,并不会真正地取消协程。要真正地取消协程,必须先在协程中用 select 的 case 分支捕获到信号 <-ctx.Done。
下面的例子演示了在协程 g1 中调用协程 g2 的方法,根据 context 包提供的功能可知,在取消协程 g1 后,会自动取消与之关联的协程 g2。
func g1(ctx context.Context) { ctx2,_:=context.WithCancel(ctx) go g2(ctx2) select { case <-ctx.Done(): fmt.Println("退出g1") //cancel() return } } func g2(ctx context.Context) { select { case <-ctx.Done(): fmt.Println("退出g2") return } } func main() { ctx,cancel:=context.WithCancel(context.Background()) go g1(ctx) cancel() time.Sleep(time.Millisecond) }context.WithCancel 函数主要用于对取消敏感的操作,例如长时间运行的任务、网络请求等。通过使用 ctx 对象和 cancel 函数,程序可以在必要时取消相关操作,从而避免出现长时间等待或资源耗费巨大的情况。
需要注意的是,在取消 ctx 对象时,可能会触发一些清理操作,例如关闭文件、释放锁等。因此,需要在相关的操作中正确地处理取消信号,避免出现资源泄露或其他问题。
3) context.WithTimeout函数
超时取消使用 context.WithTimeout 函数来实现,它会返回一个新的 Context 对象,示例代码如下:ctx, cancel := context.WithTimeout(parentContext, timeoutDuration)其中,parentContext 是一个已有的 Context 对象,timeoutDuration 是一个 time.Duration 类型的值,表示超时时间。返回的 ctx 是一个新的 Context 对象,可以用于带有超时时限的操作。
context.WithTimeout 函数一般需要与 select 配合使用,其使用方式与 context.WithCancel 函数类似,示例代码如下:
func TestWithTimeout(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() //超时会自动调用 select { case <-time.After(1 * time.Second): fmt.Println("1秒超时退出") case <-ctx.Done(): //必须使用case捕获这个信号 fmt.Println("使用WithTimeout实现超时退出") fmt.Println(ctx.Err()) //输出"context deadline exceeded" } }使用 ctx 对象时,可以通过 ctx.Done 方法获取一个 chan struct{} 类型的通道,这个通道会在以下任意一件事件发生时关闭:
- 与 ctx 对象相关的父 Context 被取消。
- timeoutDuration 到达指定时间,在本例中为 500*time.Millisecond,此时 ctx.Err 方法返回一个 context deadline exceeded 错误。
当 ctx.Done 方法返回的通道关闭时,可以根据 ctx.Err 方法返回的错误信息来判断是哪种情况发生了。需要注意的是,这里要使用值语义来操作 Context,因为 Context 在调用函数的过程中可能会发生变化。ctx.Done 是一个返回通道的方法,只要一进入 select 代码段,就开始倒计时。
context.WithTimeout 函数常用于对超时敏感的操作,例如网络请求、I/O 操作等。通过设置合适的超时时间,可以避免等待时间过长而对系统性能产生负面影响。
4) context.WithDeadline函数
context.WithDeadline 函数可以用于带有超时或截止时间的操作,它会返回一个新的 Context 对象,示例代码如下:ctx, cancel := context.WithDeadline(parentContext, deadlineTime)其中,parentContext 是一个已有的 Context 对象,deadlineTime 是一个 time.Time 类型的值,表示截止日期或超时时间。返回的 ctx 是一个新的 Context 对象,用于带有超时或截止日期的操作。
使用 ctx 对象时,可以通过 ctx.Done 方法获取一个 chan struct{} 类型的通道,这个通道会在以下任意一件事件发生时关闭:
- 与 ctx 对象相关的父 Context 被取消;
- deadlineTime 到达指定时间,此时 ctx.Err 方法返回一个 context.DeadlineExceeded 错误。
context.WithTimeout 函数中的超时时间是相对时间,即从当前时间开始计算,而 context.WithDeadline 函数中的截止日期是绝对时间。它们的原理差不多,要做的都是等待某个协程在规定时间范围内完成工作。笔者更喜欢用前一种方式来实现超时。
context.WithDeadline 函数常用于对超时敏感的操作,例如网络请求、I/O操作等。在这些操作中,如果等待的时间过长,可能会对系统性能产生负面影响。
以上就是 context 包中各种函数的说明,它们可以保存值,也可以把超时的任务取消。
Go语言Context与请求超时
Context 除了用于取消协程,还可以用于处理 HTTP 请求超时的情况。对于一个 HTTP 请求,如果在一定的时间范围内处理不完,又没有返回任何信息,那么就会带来较差的用户体验。因此,我们可通过设置超时时间,并利用上下文的超时取消机制来让程序在超时后立即返回预先设置的信息,从而改善用户体验。在下面的示例中,我们编写了一个 HTTP 服务,如果处理一个客户端请求所花费的时间超过 3 秒,就会返回“执行超过3秒”的提示。
func main() { http.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) { ctx, cancel := context.WithTimeout(request.Context(), time.Second*3) defer cancel() c := make(chan string) go func() { //模拟一个随机的访问时间 //如果超过 3 秒就会进入 select 的 ctx.Done 分支 time.Sleep(time.Duration(rand.Intn(5)) * time.Second) c <- fmt.Sprintf("执行时间花费%d秒", n) }() select { case <-ctx.Done(): writer.Write([]byte("执行超过3秒")) case ret := <-c: writer.Write([]byte(ret)) } }) http.ListenAndServe(":8888", nil) }执行结果为:
$ curl http://localhost:8888
执行时间花费2秒%
$ curl http://localhost:8888
执行超过3秒%
还可以进一步完善代码,当达到预设的超时时间时,先不取消,而是尝试重试操作。关键代码如下:
// 设置超时时间为5秒 ctx, cancel := context.WithTimeout(context.Background(), duration) defer cancel() retryTimeout(ctx, time.Second*3, func(ctx context.Context) error { return errors.New("失败") }) } func retryTimeout( ctx context.Context, retryInterval time.Duration, check func(ctx context.Context) error) { for { if err := check(ctx); err == nil { ... return } if ctx.Err() != nil { ... return } //等待 retryInterval 秒后重试 t := time.NewTimer(retryInterval) select { case <-ctx.Done(): //超时 .... t.Stop() return case <-t.C: //重试 ... } } }这段代码会去调用 retryTimeout 函数尝试重新执行指定的操作,直到操作成功或到达最终的超时限制。该函数有如下参数:
- ctx:上下文对象,用于控制超时和取消操作。
- time.Second*3:超时时间,即操作最多可以执行的时间。
- func(ctx context.Context) error:要执行的操作,是一个接受上下文对象并返回 error 类型的函数。
retryTimeout 函数使用上下文对象 ctx 来控制超时和取消操作,并使用定时器来实现超时机制。每次重试都会等待一段时间,以避免操作过于频繁。
Context的使用总结
Go 官网提出了规范使用 Context 的建议,具体如下:1) 不要把 Context 作为结构体的成员,建议将其作为第一个参数传递给函数,通常命名为 ctx。这样做可以将上下文与函数调用明确地关联起来,以避免上下文被误用。
2) 即使方法允许,也不要传递 nil 值的 Context。因为 nil 值的 Context 不能传递与请求相关的元数据,也不能取消上下文。如果在代码中不小心使用了 nil 值的 Context,就会导致应用程序出现不可预料的错误或者异常。如果不确定应该使用哪个 Context,可以使用 context.TODO,它会返回一个空的、不可取消的 Context。这个 Context 可以在应用程序的开发阶段作为占位符或者提示使用,开发者可将其替换为具体的 Context。
3) 使用 context.Context 接口的 Value 相关方法时,建议只用其传递与请求相关的元数据,而不要用它来传递可选的参数。因为 Value 方法是基于链式调用的,每次调用都会返回一个新的 Context 对象,而这个新的 Context 对象会包含原有的值和新的键值对,这会导致 Context 对象不断增大,最终会影响应用程序的性能。另外,使用 Value 方法传递可选参数也不利于代码的维护和扩展。
4) 一个 Context 可以传递给多个协程,这样,在函数或方法内部就可以使用 Context 中的元数据来完成相关的操作。由于 Context 是并发安全的,多个协程可以同时访问同一个 Context 对象,因此不会产生竞争或者死锁。此外,因为 Context 支持超时和取消机制,所以可以在任意一个协程中取消请求,从而避免出现资源泄露和无用的计算等情况。