context取消与超时
2025/12/14大约 4 分钟
本篇是你 Go 并发学习的 第 11 天:context 取消任务、超时、派生 context,会大量结合 Java Future / CompletableFuture / 线程中断 来对比讲解。
- 你的背景:Java 程序员
- 操作系统:Linux Mint XFCE
- Go 版本:go1.22.2 linux/amd64
- 项目目录示例:
/home/liumangmang/GolandProjects/go-context-practice
📌 标题
Go 并发进阶:context 取消任务、超时与派生 Context(对标 Java Future)
✅ 步骤 1:创建练习项目
cd /home/liumangmang/GolandProjects
mkdir go-context-practice && cd go-context-practice
go mod init go-context-practice你也可以复用前几天的项目,只要在同一个 Go module 里新建
.go文件即可。
✅ 步骤 2:context.WithCancel —— 类比 Java 的“取消令牌”
Java 类比:
- 好比你手里有一个
CancellationToken,传给各个线程; - 当某个时刻调用
token.cancel(),所有线程都会检测到“被取消了”,然后主动退出。
创建 cancel_basic.go:
nano cancel_basic.go粘贴:
package main
import (
"context"
"fmt"
"time"
)
// 模拟一个可被取消的循环任务
func worker(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Println(name, "收到取消信号:", ctx.Err())
return
default:
fmt.Println(name, "还在干活...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go worker(ctx, "worker-1")
go worker(ctx, "worker-2")
time.Sleep(2 * time.Second)
fmt.Println("main: 决定取消所有 worker")
cancel() // 发出取消信号
time.Sleep(1 * time.Second)
fmt.Println("main 结束")
}运行:
go run cancel_basic.go你会看到两个 worker 一开始持续打印“还在干活...”,当 cancel() 被调用后,都会打印“收到取消信号”。
对比 Java:相当于你在循环里不断检查
Thread.currentThread().isInterrupted(),一旦发现中断标记就退出;只不过 Go 用的是ctx.Done()channel 来统一表达取消。
✅ 步骤 3:context.WithTimeout / context.WithDeadline —— 类比 Future.get(timeout)
Java 类比:
future.get(2, TimeUnit.SECONDS):超过 2 秒还没完成就抛 TimeoutException;- Go 里常见模式:用
context.WithTimeout传给函数,由函数内部决定是否提前结束。
创建 timeout_with_context.go:
nano timeout_with_context.go粘贴:
package main
import (
"context"
"fmt"
"time"
)
// 模拟一个可能很慢的操作
func slowJob(ctx context.Context) error {
for i := 1; i <= 5; i++ {
select {
case <-ctx.Done():
fmt.Println("slowJob 被取消:", ctx.Err())
return ctx.Err()
default:
fmt.Println("slowJob 进行中 step", i)
time.Sleep(1 * time.Second)
}
}
fmt.Println("slowJob 正常完成")
return nil
}
func main() {
// 最多给 slowJob 3 秒时间
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
fmt.Println("开始执行 slowJob,超时时间 3 秒...")
if err := slowJob(ctx); err != nil {
fmt.Println("结束,原因:", err)
return
}
fmt.Println("结束:正常完成")
}运行:
go run timeout_with_context.go你会看到 slowJob 大约只执行 3 步,就因为 context 超时而退出。
对比 Java:就像
future.get(3, TimeUnit.SECONDS)抛了 TimeoutException,只不过在 Go 里,业务逻辑自己决定如何处理超时(比如打印日志、回滚状态、释放资源等)。
✅ 步骤 4:派生 Context:上下游“任务树”的取消传播
Java 类比:
- Web 请求入口创建一个“请求上下文”;
- 请求处理过程中再创建各种“子任务”,都共享相同的取消信号;
- 当用户取消请求时,整棵调用树都应该尽快结束。
创建 derived_context.go:
nano derived_context.go粘贴:
package main
import (
"context"
"fmt"
"time"
)
func subTask(ctx context.Context, name string, d time.Duration) {
select {
case <-ctx.Done():
fmt.Println(name, "提前被取消:", ctx.Err())
case <-time.After(d):
fmt.Println(name, "完成,用时", d)
}
}
func mainTask(ctx context.Context) {
// 从上游 ctx 派生两个子 context
ctx1, cancel1 := context.WithCancel(ctx)
defer cancel1()
ctx2, cancel2 := context.WithTimeout(ctx, 2*time.Second)
defer cancel2()
go subTask(ctx1, "subTask-1", 5*time.Second)
go subTask(ctx2, "subTask-2", 5*time.Second)
time.Sleep(1 * time.Second)
fmt.Println("mainTask: 主动取消 subTask-1 的 ctx1")
cancel1()
// 等待一会儿,看 subTask-2 是否因超时被取消
time.Sleep(3 * time.Second)
}
func main() {
root := context.Background()
fmt.Println("开始 mainTask...")
mainTask(root)
fmt.Println("main 结束")
}观察输出:
subTask-1会因为cancel1()被提前取消;subTask-2会因为自己的WithTimeout超时而被取消;- 两个子 context 都是从同一个上游
ctx(这里是 Background)派生出来的。
心法:不要在 goroutine 里“平白无故”创建 Background context,而是尽量从上游传下来的 ctx 派生。
✅ 步骤 5:最佳实践 & Java 对照
函数签名规范:
- Go:
func DoSomething(ctx context.Context, req *Request) (*Response, error) - Java 中常见:
doSomething(Request request, CancellationToken token)或基于框架内置上下文。
- Go:
不要把 ctx 存成 struct 字段:
- ctx 是“请求级别”的东西,应该顺着调用链传,不要挂在全局变量或长生命周期对象上。
取消是“协作式”的:
- 无论是 Java 的线程中断,还是 Go 的 context,都不会强杀你的逻辑,只是提供一个“你该停了”的信号。
- 业务代码必须自己写
select { case <-ctx.Done(): ... }这样的检查。
📚 今日小结与练习
你已经掌握:
context.WithCancel:类似“取消令牌”,适合手动取消整条任务链。context.WithTimeout/WithDeadline:携带超时信息,某个时刻之后自动取消。- 派生 context:从上游 request 派生子 context,实现“树状任务”的统一退出。
练习建议:
- 把第 10 天的
select_timeout.go改造为使用context.WithTimeout实现超时控制。 - 写一个“批量 HTTP 请求”的小工具,给每个请求传入相同的
ctx,当任意一个失败时取消全部。 - 在“并发爬虫”(第 14 天)中,为每个批次增加总超时时间,超时自动停止抓取。
- 把第 10 天的
