背景与需求
在Go语言的http包中,每个请求都会由一个独立的goroutine处理,通常还会额外启动子goroutine来访问后端服务(如数据库或RPC)。这些goroutine需要共享与请求相关的数据(如认证信息、截止时间等)。当请求被取消或超时时,所有相关的goroutine都应迅速退出以释放资源。
为了解决这一问题,Go引入了context包,用于管理请求范围内的操作。
优雅退出goroutine的演变
1. 直接退出
此版本无法接收外部指令以控制goroutine的退出。
func worker() {
for {
fmt.Println("worker")
time.Sleep(time.Second)
}
wg.Done()
}
func main() {
wg.Add(1)
go worker()
wg.Wait()
fmt.Println("over")
}
2. 全局变量控制退出
通过全局变量通知goroutine退出,但存在扩展性和维护性问题。
var exit bool
func worker() {
for {
fmt.Println("worker")
time.Sleep(time.Second)
if exit {
break
}
}
wg.Done()
}
func main() {
wg.Add(1)
go worker()
time.Sleep(3 * time.Second)
exit = true
wg.Wait()
fmt.Println("over")
}
3. 通道方式
通过channel通知goroutine退出,相较全局变量更安全,但跨包调用时需额外维护全局的channel。
func worker(exitChan chan struct{}) {
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Second)
select {
case <-exitChan:
break LOOP
default:
}
}
wg.Done()
}
func main() {
exitChan := make(chan struct{})
wg.Add(1)
go worker(exitChan)
time.Sleep(3 * time.Second)
exitChan <- struct{}{}
close(exitChan)
wg.Wait()
fmt.Println("over")
}
官方推荐:Context的引入
context包从Go 1.7开始成为标准库,专为简化单个请求范围内的goroutine通信与取消操作而设计。以下是使用context的实现:
使用context.Context控制退出
func worker(ctx context.Context) {
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Second)
select {
case <-ctx.Done(): // 监听取消信号
break LOOP
default:
}
}
wg.Done()
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
wg.Add(1)
go worker(ctx)
time.Sleep(3 * time.Second)
cancel() // 通知goroutine退出
wg.Wait()
fmt.Println("over")
}
如果子goroutine中还需要启动新的goroutine,只需继续传递ctx:
func worker(ctx context.Context) {
go worker2(ctx)
LOOP:
for {
fmt.Println("worker")
time.Sleep(time.Second)
select {
case <-ctx.Done():
break LOOP
default:
}
}
wg.Done()
}
func worker2(ctx context.Context) {
LOOP:
for {
fmt.Println("worker2")
time.Sleep(time.Second)
select {
case <-ctx.Done():
break LOOP
default:
}
}
}
Context接口与方法
context.Context是一个接口,主要方法包括:
Deadline()
返回Context的截止时间。Done()
返回一个channel,用于通知goroutine结束。Err()
返回Context结束的原因(如被取消或超时)。Value()
用于存储请求范围内的数据,适合跨API传递信息。
Context的内置函数
1. Background()与TODO()
Background()
通常作为顶层Context,适用于main函数、初始化或测试场景。TODO()
占位使用,当不知道该使用何种Context时调用。
2. With系列函数
-
WithCancel创建可取消的Context:ctx, cancel := context.WithCancel(context.Background()) defer cancel() -
WithDeadline设置绝对超时时间:ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Second)) defer cancel() -
WithTimeout设置相对超时时间:ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() -
WithValue在Context中存储键值对:ctx := context.WithValue(context.Background(), "key", "value") fmt.Println(ctx.Value("key"))
使用Context的注意事项
- 推荐以参数的方式显示传递Context
- 以Context作为参数的函数方法,应该把Context作为第一个参数。
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
- Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
- Context是线程安全的,可以放心的在多个goroutine中传递