在并发编程中,合理地控制协程(goroutine)的生命周期是保证程序稳定性和资源可控使用的关键。Go语言标准库中的
context
包正是为了解决这一问题而生。它为我们提供了取消信号、超时控制、请求作用域的值传递等功能。
本文将通过一个实际案例,演示如何使用 context
控制协程的取消,避免资源泄露,实现优雅退出。
一、什么是 context
context
是 Go 1.7 起加入标准库的一个重要包,用于跨 API 边界传递取消信号、超时时间、截止时间等信息。
主要接口定义如下:
scss
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
其中最关键的是:
- •
Done()
:返回一个 channel,当 context 被取消或超时关闭时,该 channel 会被关闭; - •
Err()
:返回取消的原因,例如context.Canceled
或context.DeadlineExceeded
。
二、常见创建方式
Go 提供了以下常用方式创建 context:
less
ctx := context.Background() // 最顶层、永不取消的 context
ctx, cancel := context.WithCancel(parent) // 手动调用 cancel() 取消
ctx, cancel := context.WithTimeout(parent, 3*time.Second) // 指定超时时间
ctx, cancel := context.WithDeadline(parent, time.Now().Add(3*time.Second)) // 到期时间点
这些 context 都可以传递到协程中,通过 ctx.Done()
控制协程的停止。
三、实战案例:使用 context
控制任务协程
场景描述
假设我们要运行一个任务,该任务每秒输出一次"正在处理",但当主程序在某个时机需要终止它(比如点击"停止按钮"或超时),我们要优雅地通知协程退出。
示例代码
go
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("worker 任务被取消:", ctx.Err())
return
default:
fmt.Println("worker 正在处理任务...")
time.Sleep(1 * time.Second)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx)
// 主线程运行5秒后取消任务
time.Sleep(5 * time.Second)
cancel()
// 等待协程打印结束语
time.Sleep(1 * time.Second)
fmt.Println("主程序退出")
}
输出结果
erlang
worker 正在处理任务...
worker 正在处理任务...
worker 正在处理任务...
worker 正在处理任务...
worker 正在处理任务...
worker 任务被取消: context canceled
主程序退出
可以看到,主程序通过 cancel()
取消了 context,协程立即响应退出了。
四、使用 context.WithTimeout
实现超时控制
除了手动调用 cancel
,我们还可以通过设定超时时间自动取消任务。
css
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
go worker(ctx)
time.Sleep(5 * time.Second) // 主线程等待更久,看任务是否能自动终止
fmt.Println("主程序退出")
}
运行结果类似:
erlang
worker 正在处理任务...
worker 正在处理任务...
worker 正在处理任务...
worker 任务被取消: context deadline exceeded
主程序退出
这表示:即使主程序没有主动调用 cancel()
,协程也在 3 秒后收到 context 超时通知并正常退出。
五、多个协程共享一个 context
我们可以启动多个协程,并使用同一个 context
控制它们:
css
func main() {
ctx, cancel := context.WithCancel(context.Background())
for i := 1; i <= 3; i++ {
go func(id int) {
for {
select {
case <-ctx.Done():
fmt.Printf("协程 %d 接收到取消信号\n", id)
return
default:
fmt.Printf("协程 %d 正在工作\n", id)
time.Sleep(1 * time.Second)
}
}
}(i)
}
time.Sleep(4 * time.Second)
cancel()
time.Sleep(1 * time.Second)
fmt.Println("主程序退出")
}
输出:
协程 1 正在工作
协程 2 正在工作
协程 3 正在工作
...(多次输出)
协程 1 接收到取消信号
协程 2 接收到取消信号
协程 3 接收到取消信号
主程序退出
这样我们实现了"一键终止所有协程"。
六、最佳实践建议
- • 永远使用
context.WithCancel
/WithTimeout
返回的cancel()
函数,不要忘记defer cancel()
; - • 在需要可控中止的任务(如网络请求、数据库操作、循环处理)中传入 context;
- • 对
ctx.Done()
的监听要放在协程内部适当位置,防止资源泄露; - • 在请求链中传递 context,以实现链路级别的取消与超时控制。
七、结语
context
是 Go 并发控制中不可或缺的利器。它不仅解决了协程取消的痛点,还能为任务设置统一的生命周期控制逻辑,是构建高可靠网络服务、后台任务系统、爬虫等并发程序的基础设施。
如果你还没有在项目中大量使用它,不妨从今天开始重构代码,使用 context
管理协程生命周期,让你的 Go 程序更稳定、更可控!