本文总结了 Go 标准库中 context 的常见用法,包括创建、传递、取消、超时部分场景。
1.context包负责Go当中的上下文传递机制
1.1 问题场景
- 一个 HTTP 请求可能启动多个 goroutine(调用下游服务、查数据库、写日志)
- 如果客户端断开连接,需要取消所有相关操作,避免浪费资源
- 如果某个操作超时,需要及时停止,而不是一直等待
1.2 传统方式的问题
- 没有统一的取消机制,每个 goroutine 需要自己管理停止逻辑
- 难以在调用链中传递取消信号
1.3 context 的价值
- 统一取消机制:一个 ctx.Done() 可以通知所有相关 goroutine
- 自动传播:子 context 会继承父 context 的取消状态
- 超时控制:可以设置超时时间,自动取消
2.context的接口实现
1 2 3 4 5 6 7
| type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any }
|
3.context的创建
1 2 3 4 5 6 7 8 9 10
| ctx1 := context.Background() ctx2 := context.TODO()
<-ctx1.Done() <-ctx2.Done()
|
4.context的传递机制
4.1 父子继承
1 2 3 4 5 6 7 8 9 10
| func main() { ctx1 := context.TODO() ctx2 := context.Background() ctx1 = context.WithValue(ctx1, "name", "father") ctx1 = context.WithValue(ctx2, "age", 18) fmt.Println(ctx1.Value("name"), ctx1.Value("age")) fmt.Println(ctx2.Value("name"), ctx2.Value("age")) }
|
1 2 3 4 5 6 7 8 9 10
| func main() { ctx1 := context.TODO() ctx2 := context.Background() ctx1 = context.WithValue(ctx1, "name", "father") ctx1 = context.WithValue(ctx1, "age", 18) fmt.Println(ctx1.Value("name"), ctx1.Value("age")) fmt.Println(ctx2.Value("name"), ctx2.Value("age")) }
|
4.2 更加直观的父子继承
1 2 3 4 5 6 7 8 9
| func main() { ctx1 := context.TODO() ctx2 := context.Background() ctx1 = context.WithValue(ctx1, "name", "father") ctx2 = context.WithValue(ctx1, "name", "son")
fmt.Println(ctx1.Value("name")) fmt.Println(ctx2.Value("name")) }
|
5.context的取消机制
context.WithCancel 函数能够从 context.Context 中衍生出一个新的子上下文并返回用于取消该上下文的函数。
一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号。
先来个简单的 demo 展示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| func work(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("ctx done") return default: fmt.Println("working") time.Sleep(1 * time.Second) } } }
func main() { ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(5 * time.Second) cancel() }()
work(ctx) }
|
五秒 working 之后 done 符合预期。
值得注意的是为什么调用取消之后 ctx.Done() 就会产生返回值呢,这跟 golang 的 channel 特性有关:当从一个关闭的 channel 读取时会读取“零值”,所以能够读取到数据。
6.context的截止时间机制
1 2 3 4 5 6 7 8 9 10 11
| func main() { ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) defer cancel()
select { case <-time.After(8 * time.Second): fmt.Println("任务执行完成") case <-ctx.Done(): fmt.Println("任务被取消:", ctx.Err()) } }
|
Deadline 更适合 整个工作流程 的截止时间(如批处理任务必须在午夜前完成、必须在事务开始 30 多秒之后提交)。
7.context的超时机制以及继承关系
当一个 Context 设置超时后,它会创建 一个新的 Context 节点 在上下文树中,超时时间会 向下传播给所有子上下文。
规则1:子上下文不能延长父上下文的超时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| func main() { rootCtx := context.Background()
parentCtx, parentCancel := context.WithTimeout(rootCtx, 5*time.Second) defer parentCancel()
childCtx, childCancel := context.WithTimeout(parentCtx, 10*time.Second) defer childCancel()
checkDeadline(parentCtx, "parent") checkDeadline(childCtx, "child") }
func checkDeadline(ctx context.Context, name string) { deadline, ok := ctx.Deadline() if ok { remaining := time.Until(deadline) fmt.Printf("%s: 截止时间 %v, 剩余 %v\n", name, deadline.Format("15:04:05"), remaining) } }
|
规则2:子上下文可以设置更短的超时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| func main() { rootCtx := context.Background()
parentCtx, parentCancel := context.WithTimeout(rootCtx, 5*time.Second) defer parentCancel()
childCtx, childCancel := context.WithTimeout(parentCtx, 2*time.Second) defer childCancel()
checkDeadline(parentCtx, "parent") checkDeadline(childCtx, "child") }
func checkDeadline(ctx context.Context, name string) { deadline, ok := ctx.Deadline() if ok { remaining := time.Until(deadline) fmt.Printf("%s: 截止时间 %v, 剩余 %v\n", name, deadline.Format("15:04:05"), remaining) } }
|
当我们在 http 请求链当中就可以设置分层严格的请求时间。