在Go语言里,Context是用来在goroutine之间传递取消信号、超时和元数据的标准方式。当我们需要中断一个正在进行的HTTP请求或者后台任务时,最干净的做法就是通过Context来通知相关代码停止工作。理解请求取消机制,有助于写出更健壮、不泄漏资源的程序。

为什么需要请求取消机制
如果不支持取消,一个已经失去意义的请求可能还在占用连接、CPU和内存。比如用户关闭了页面,但服务端仍在查询数据库。使用Context可以让这些任务及时收到信号并退出。
创建可取消的Context
最基础的方式是使用context.WithCancel。它返回子Context和一个取消函数,调用取消函数就会让该Context进入结束状态。
package main
import (
"context"
"fmt"
"time"
)
func main() {
// 创建可取消的Context
ctx, cancel := context.WithCancel(context.Background())
go func() {
// 模拟工作,监听取消信号
for {
select {
case <-ctx.Done():
fmt.Println("收到取消信号,退出")
return
default:
fmt.Println("工作中...")
time.Sleep(500 * time.Millisecond)
}
}
}()
time.Sleep(2 * time.Second)
cancel() // 主动取消
time.Sleep(1 * time.Second)
}
在HTTP请求中使用Context取消
Go的net/http包天然支持Context。我们可以把带超时的Context传给请求,时间一到请求自动取消。
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
// 设置3秒超时
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://ipipp.com/api", nil)
if err != nil {
fmt.Println("创建请求失败:", err)
return
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("请求出错:", err)
return
}
defer resp.Body.Close()
fmt.Println("状态码:", resp.StatusCode)
}
Context取消的传递规则
当一个Context被取消,所有由它派生的子Context都会收到取消信号。这形成了树状的取消传播结构。
- 父Context取消,子Context立即Done
- 子Context自己取消,不影响父Context
- 建议把Context作为函数第一个参数传入,命名为ctx
在服务端接收取消信号
HTTP服务端可以从请求对象拿到Context,当客户端断开连接时,该Context会被取消,服务端可据此停止处理。
package main
import (
"fmt"
"net/http"
"time"
)
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
select {
case <-time.After(5 * time.Second):
fmt.Fprintln(w, "处理完成")
case <-ctx.Done():
fmt.Println("客户端取消:", ctx.Err())
}
}
func main() {
http.HandleFunc("/", handler)
http.ListenAndServe(":8080", nil)
}
常见注意事项
使用Context取消请求时,应注意不要把Context存进结构体长期持有,也不要用Context传递可选参数。取消函数通常要defer调用,防止遗漏。
| 函数 | 作用 |
|---|---|
| WithCancel | 手动取消 |
| WithTimeout | 超时自动取消 |
| WithDeadline | 到达指定时间取消 |
通过合理使用Context,Go程序的请求取消机制可以变得清晰且可维护,有效避免资源浪费与协程泄漏问题。