24.Context
24.1 Context 定义
在学习 Context 定义之前,我们先来看看一组生活中很平常的对话。
- A:
你有看过上周的比赛吗?
- B:
嗯,看了。
- A:
比赛很精彩,我相信他们一定会赢得下一场比赛的。
- B:
是吗?我赌 1 块钱
从上面的示例可以得知 A 和 B 在谈论某场比赛,在上周其中一支队伍赢得了比赛,而且有很大概率赢得下一场比赛。但从这些非常简短的信息中,我们却无法得知是哪支队伍,也不知道是什么类型的比赛。
而如果能提供一些额外的辅助信息,将上面的对话信息串联起来,就有助于我们快速理解。假设这个比赛发生上海,再结合当前的时间信息,我们就能猜出可能足球比赛。而如果这个比赛发生在广州,则可能是篮球比赛等等。在添加前面这些信息后,再结合时间和比赛结果,我们便能很快知道上面的谈话,何时、何地、谁赢得了比赛。而帮助我们理解和获取这些信息,需要结合前后的对话信息的,就是 Context。
Context 中文一般翻译为 上下文。Go 语言在 1.7 版本中引入 context 包。它提供了一种比使用通道更加简洁的方式来管理跨 goroutine 的取消和超时行为。虽然 context 包涉及的范围有限,API个数也不多,但它在被引入到 Go 语言时仍受到了欢迎。context 包中定义了 context.Context 类型。该类型可以在API边界和进程之间携带截止时间、取消信号和其他请求范围内的值。如下所示:
$ go doc -short context
var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}
func AfterFunc(ctx Context, f func()) (stop func() bool)
func Cause(c Context) error
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc)
type CancelCauseFunc func(cause error)
type CancelFunc func()
type Context interface{ ... }
func Background() Context
func TODO() Context
func WithValue(parent Context, key, val any) Context
func WithoutCancel(parent Context) Context
Context 主要用于控制应用程序中的并发子系统。后续,我们将学习 Context 的各种不同行为,例如取消、超时和值传递等。
24.2 Context 使用场景
Context 的使用场景主要有以下几种方式。
24.2.1 取消传播
为了更好的理解这个用法,我们使用一个虚拟项目来进行讲解。老板 A 拿下了一个政府批准大型游乐项目,于是开始按照计划逐步推进,向各个下游原材料商预订了各种建设所需的原材料。但在数月之后,因为某些原因,这个项目被叫停了。老板 A 虽然很生气,但也不得不取消各类订单。于是逐个跟下游的原材料商电话协商退货事宜,有一些原材料商又跟下游原材料商预订了原材料。大家对这个项目的叫停感觉非常无奈。
现在让我们来假设一下,如果老板 A 未取消这些下游原材料商的订单,这些下游的原材料商将按原计划继续准备,但却要面临无法支持货款的问题,也会导致极大的资料浪费。如果老板 A 取消订单,则可以通过自顶向下,逐层传递取消订单的信息。如下所示:

在人类的日常活动中,我们总有一种方式来通知各种突发情况而导致取消的事情。而在计算机科学里面,我们也可以借鉴这种取消传播的方式,从而引入context 机制。比如,当我们发送一个HTTP请求之后,而在客户端取消连接之后,服务端也可以取消后面所需要各种调用链。
24.2.2 请求域内传输数据
当一个请求被发送至服务器之后,Web Server 中的响应函数并不是单独工作,这个响应函数可能还调用了其他函数,而其他函数又可能调用了其他的函数,从而形成一个调用链。在微服务模式中,发生一个请求之后,会产生一个新的请求,而这个新的请求又会调用另一个微服务的请求等等,从而形成一个调用链,称之为调用栈。在这里,我们将会理解为什么在调用栈之间传输数据是非常有用的。
以下日常购物为例。一般将会经历以下步骤:
- 用户通过浏览器或APP登录
- 填写登录信息
- 向服务端发送鉴权信息,服务端调用鉴权服务
- 服务端构建个人账户页面(例如通过template)并发送到用户端
- 如果用户查询最近订单,服务端将调用订单服务查询并返回给客户端
以上整个流程图示例如下所示:

以上流程图中,哪些信息可以添加到 Context 中呢?
- 1.将用户设备类型信息添加到请求中
如果用户设备为手机类型,为改善用户体验,我们可以返回一些适应于手机类型的页面,查询最近订单信息,可以返回最近10个订单信息等等
- 2.添加已经通过鉴权的UserID信息
- 3.添加用户发起请求的IP信息
通过IP信息,鉴权模块可以阻止一些异常登录信息
- 4.添加RequestID信息
RequestID可以在各个服务模块中传输,通过这些RequestID,在出现问题时,开发人员可以使用这些ID信息在日志中进行跟踪,从而更好的修复问题。
24.2.3 设置截止和超时时间
截止日期通常是指任务完成的最后时间点,超时时间跟它非常像,通常是指一段最大可持续时间,而这个时间点会非常精确。示例如下所示:
- Client 向 Server发起请求,指定超时时间为 3 秒
- 你在Context中设定了超时时间为 3 秒,在3秒之后,Client便会放弃连接
- Server 侧在 3 秒之后也不在等待 Client的请求,主动释放连接和资源,从而避免了无效的等待和资源占用
24.3 Context 接口
在 context 包中暴露出来的接口方法如下所示:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
context 包是一种标准的链表 数据结构。为了更好的理解 context 是如何工作的,我们来简要了解一下链表。
链表是一类数据元素的集合,而对于存储的数据类型没有严格要求,可以是 int、string、struct、float 等等。链表中元素称之为节点,每一个节点包含数据域和指针域,其中指针域中存储的是下一个节点的内存地址。
链表具备以下特性
- 第一个节点称之为头部节点,最后一个节点称这为尾部节点
- 除头部节点和尾部节点外,每个节点都会有一个子节点和父节点
示意图如下所示:


24.3.1 root context-Background
大部分程序中,我们通常都会创建一个叫 Background 做为 root context。例如我们在main函数中,创建一个root context,示例如下所示:
ctx := context.Background()
在调用 Background 之后,会返回一个指向 empty context 的指针,即在内部实现中,调用 Background() 将创建一个新的 context.emptyCtx。而这个类型是没有暴露出来的,其定义如下所示:
type emptyCtx struct{}
emptyCtx实现了 Context 接口的四个方法,如下所示:
func (emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (emptyCtx) Done() <-chan struct{} {
return nil
}
func (emptyCtx) Err() error {
return nil
}
func (emptyCtx) Value(key any) any {
return nil
}
24.3.2 在函数/方法中添加context
当 root context 被创建之后,就可以将其添加到函数或方法中,示例如下所示:
func contextSample(ctx context.Context, num int) {
// ...
}
如果在函数/方法需要使用context,需要遵循以下两个约定
context 需要做为函数/方法的第一个参数context 的参数名称为 ctx
24.3.3 context 派生
前面我们创建了一个空的root context,它什么也不做。但我们可以基于这个空的context 派生出其他的子 context。如下所示:

基于 context 派生功能,可以使用以下几种方法
- context.WithCancel()
- context.WithTimeout()
- context.WithDeadline()
- context.WithValue()
24.3.3.1 WithCancel
WithCancel()函数仅接受一个名为 parent 的参数。这个参数表示即将派生的context。其定义如下所示:
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
WithCancel() 函数返回下一个 context 和一个函数类型的 cancel ,其中 CancelFunc 是 context 包中自定义的类型,,其定义如下所示:
type CancelFunc func()
通过调用 WithCancel,可以实现取消操作。示例如下所示:
ctx,cancel:=context.WithCancel(context.Background())
defer cancel()
24.3.3.1 WithTimeout/WithDeadline
- 超时时间: 表示应用程序可以正常处理任务的最大持续时间,对于任何处理任务时,时间不固定的情况下,我们都需要添加一个超时时间。如果没有设定超时时间,则很有可能程序会无限制的等待程序处理完成。示例用法如下所示:
ctx,cancel:=context.WithTimeout(context.Background(),5 * time.Second)
defer cancel()
- 截止时间:通常是指一个时间点,在设置截止时间之后,程序处理不会超过指定的截止时间
deadline:=time.Date(2025,11,30,8,23,0,0,time.UTC)
ctx,cancel:=context.WithDeadline(context.Background(),deadline)
defer cancel()
24.4 Context 案例
我们以一个简单的HTT为示例,Client 向 Server 发送请求,并展示响应信息。首先展示没有 Context 的场景,再展示添加 Context 的场景。
24.4.1 测试代码
24.4.1.1 Client
package main
import (
"log"
"net/http"
)
func main() {
req, err := http.NewRequest("GET", "http://127.0.0.1:9090", nil)
if err != nil {
panic(err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
log.Printf("Received Response: %+v\n", resp)
}
24.4.1.2 Server
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("Recevied request")
time.Sleep(3 * time.Second)
fmt.Fprintf(w, "Response Demo")
log.Println("Response send")
})
err := http.ListenAndServe(":9090", nil)
if err != nil {
panic(err)
}
}
24.4.2 无 Context 场景
运行结果如下所示:
$ go run server/main.go
2025/11/16 21:58:02 Recevied request
2025/11/16 21:58:05 Response send
$ go run client/main.go
2025/11/16 21:58:05 Received Response: &{Status:200 OK StatusCode:200 Proto:HTTP/1.1 ProtoMajor:1 ProtoMinor:1 Header:map[Content-Length:[13] Content-Type:[text/plain; charset=utf-8] Date:[Sun, 16 Nov 2025 13:58:05 GMT]] Body:0xc000114080 ContentLength:13 TransferEncoding:[] Close:false Uncompressed:false Trailer:map[] Request:0xc0001ee000 TLS:<nil>}
通过打印的日志,可以看到client在发送请求之后,3秒之后才收到server返回的响应。如果server端继续增加等待时间,则client也会同步一直等待下去。而这并不是一个很好的设计。相反告诉client出错的信息比无限制等待才是最佳办法。
24.4.3 client 侧添加 context
在保留已有代码的基础上,我们可以添加一个root context,如下所示:
rootCtx := context.Background()
然后再派生一个新的context,命名为 ctx:
ctx, cancel := context.WithTimeout(rootCtx, 50*time.Millisecond)
给函数 WithTimeout() 传入两个参数 rootCtx 和 50*time.Millisecond ,第二个参数表示超时时间为 50 毫秒。
在日常项目,超时时间通常是通过配置文件配置和读取,这里仅仅是为了演示
调用返回函数 cancel 可以告诉子进程需要停止运行,并释放分配和占用的资源。为了确保程序能够调用函数 cancel ,通过使用 defer,如下所示:
defer cancel()
最终修改后的代码如下所示:
package main
import (
"context"
"log"
"net/http"
"time"
)
func main() {
// 创建 root context
rootCtx := context.Background()
// NewRequest wraps [NewRequestWithContext] using [context.Background].
// func NewRequest(method, url string, body io.Reader) (*Request, error) {
// return NewRequestWithContext(context.Background(), method, url, body)
// }
req, err := http.NewRequest("GET", "http://127.0.0.1:9090", nil)
if err != nil {
panic(err)
}
// 创建 context
ctx, cancel := context.WithTimeout(rootCtx, 50*time.Millisecond)
defer cancel()
req = req.WithContext(ctx)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
log.Printf("Received Response: %+v\n", resp)
}
运行结果如下所示:
$ go run server/main.go
2025/11/16 22:25:40 Recevied request
2025/11/16 22:25:43 Response send
$ go run client/main.go
panic: Get "http://127.0.0.1:9090": context deadline exceeded
goroutine 1 [running]:
main.main()
C:/Users/Surpass/GolangProjets/src/go-learning-note/24/2402-Client-With-Context/client/main.go:31 +0x1fd
exit status 2
通过打印的日志可以看到,Server在3秒之后,正常发送响应,但client却出现了错误,原因是因为超时。尽管client因超时已经取消请求,但server依然正常执行任务流程,为避免这种情况产生,我们需要找到能够在client和server共享context的方法。
24.4.4 server 侧添加 context
24.4.4.1 Headers
一个 HTTP 请求通常包含请求体、请求参数和一组请求头组成。当发送请求时,Go通常不会传递任何关于 context 的信息。如果想看看发送请求时,都传递了哪些请求头信息,可以使用以下代码打印请求头信息。
for name, headers := range r.Header {
for _, h := range headers {
fmt.Printf("%s:%s\n", name, h)
}
}
打印的请求头信息如下所示:
User-Agent:Go-http-client/1.1
Accept-Encoding:gzip
打印出来的请求头信息有两个,分别表示client当前使用的User-Agent信息和可接受gzip的压缩数据,并没有包含超时相关的信息。但当我们去看http.Request 对象时,却发现有一个方法 Context。 该方法将会获取 request 中的 context 信息。如果未获取到,则返回一个 emptyCtx。源码如下所示:
type Request struct {
Method string
URL *url.URL
Proto string
// ...
ctx context.Context
}
func (r *Request) Context() context.Context {
if r.ctx != nil {
return r.ctx
}
return context.Background()
}
通过文档,可以得知在client关闭连接时,则ctx也执行取消动作。也就意味着在Go Server内部已经实现了,在客户端关闭连接后,将会调用cancle函数。
24.4.4.2 函数 doWork
假设server有一个计算密集型的函数 doWork 这个函数仅仅是演示 CPU密集型 任务一个示例,并无太多业务逻辑,其工作流程如下所示:

函数doWork将会在一个独立的goroutine中运行,具有两个参数,类型分别为context和channel,其定义如下所示:
func doWork(ctx context.Context, resChan chan int) {
log.Println("[doWork] launch the doWrok")
sum := 0
for {
log.Println("[doWork] one iteration")
time.Sleep(1 * time.Millisecond)
select {
case <-ctx.Done():
log.Println("[doWork] ctx Done is recevied inside doWork")
return
default:
sum++
if sum > 1000 {
log.Panicln("[doWork] sum has reached 1000")
resChan <- sum
return
}
}
}
}
- 如果channel返回
ctx.Done()则代表着通道被关闭,即意味着我们收到了工作已经完成的指令,在这个case中,我们将打断循环,打印日志并返回 - 如果前面的case都没有执行,则执行default里面的逻辑,将执行自增逻辑,如果总和已经达到1000,将发送结果到通道resChan中
函数 doWork 的执行流程如下所示:

24.4.4.3 Server Handler
下面来看看在server handler内部如何使用函数doWork
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("[Handler] Recevied request")
// 获取 Request 中的 context
rCtx := r.Context()
// 创建存储结果的 Channel
resChan := make(chan int)
// 调用函数 doWork
go doWork(rCtx, resChan)
// 等待结果
// 1. client 关闭连接
// 2. 函数 doWork 运行完成
select {
case <-rCtx.Done():
log.Println("[Handler] context canceld in main handler,client has disconnected")
return
case result := <-resChan:
log.Println("[Handler] Recevied 1000")
log.Println("[Handler] Send Response")
fmt.Fprintf(w, "Response %d", result)
return
}
})
err := http.ListenAndServe(":9090", nil)
if err != nil {
panic(err)
}
}
func doWork(ctx context.Context, resChan chan int) {
log.Println("[doWork] launch the doWrok")
sum := 0
for {
log.Println("[doWork] one iteration")
time.Sleep(1 * time.Millisecond)
select {
case <-ctx.Done():
log.Println("[doWork] ctx Done is recevied inside doWork")
return
default:
sum++
if sum > 1000 {
log.Println("[doWork] sum has reached 1000")
resChan <- sum
return
}
}
}
}
修改server代码,并使用request中的context。代码如下所示:
// 获取 Request 中的 context
rCtx := r.Context()
然后再创建一个整型通道resChan并与函数doWork进行通信,然后在一个单独的goroutine中运行函数doWork
// 创建存储结果的 Channel
resChan := make(chan int)
// 调用函数 doWork
go doWork(rCtx, resChan)
最后使用select来等待两种可能的运行结果
- client 关闭连接,因此cancel通道了被关闭
- 函数
doWork运行完成,我们将从通道resChan收到运行结果
运行结果如下所示:
- server
$ go run server/main.go
2025/11/16 23:51:35 [Handler] Recevied request
2025/11/16 23:51:35 [doWork] launch the doWrok
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [doWork] one iteration
2025/11/16 23:51:35 [Handler] context canceld in main handler,client has disconnected
2025/11/16 23:51:35 [doWork] ctx Done is recevied inside doWork
- client
$ go run client/main.go
panic: Get "http://127.0.0.1:9090": context deadline exceeded
goroutine 1 [running]:
main.main()
C:/Users/Surpass/GolangProjets/src/go-learning-note/24/2402-Client-With-Context/client/main.go:31 +0x1fd
exit status 2
24.4.5 WithDeadline
WithDeadline 和 WithTimeout 非常类似。如果我们去看 context 包中的源码,可以看到 WithTimeout 是被 WithDeadline 包装的一个函数,其超时时间为当前时间加上一个设定的超时时间,如下所示:
// 标准库里面 context.go
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
我们再来看看 WithDeadline 的定义,如下所示:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
return WithDeadlineCause(parent, d, nil)
}
WithDeadline 需要传入两个参数,一个父级 context 和 一个指定的时间。正如前面所言,截止时间和超时时间两个非常类似
- 超时时间表示一段持续时间
- 截止时间表示一个特定的时间节点
两者比较示意图如下所示:

基于上述比较,在有些情况中,WithDeadline 可以代替 WithTimeout,以下为标准库一个示例,如下所示:
// src/net/dnsclient_unix.go
// exchange sends a query on the connection and hopes for a response.
func (r *Resolver) exchange(ctx context.Context, server string, q dnsmessage.Question, timeout time.Duration, useTCP, ad bool) (dnsmessage.Parser, dnsmessage.Header, error) {
// ...
for _, network := range networks {
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(timeout))
defer cancel()
}
24.4.6 取消传播
先来看看以下示例:
func main() {
ctx1 := context.Background()
ctx2, cancel := context.WithCancel(ctx1)
defer cancel()
}
在以上示例代码中,先创建了一个root context: ctx1,在此基础上派生出 context.WithCancel()。Go将创建一个新的结构体,被调用的函数如下所示:
// 外部可见方法
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
// 内部可见方法,外部不可见
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := &cancelCtx{}
c.propagateCancel(parent, c)
return c
}
func (c *cancelCtx) propagateCancel(parent Context, child canceler) {
c.Context = parent
done := parent.Done()
if done == nil {
return // parent is never canceled
}
// 后面代码省略
}
在执行 WithCancel 时将创建一个新的 cancelCtx ,将 root context 嵌入在里面,其定义代码如下所示:
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err atomic.Value // set to non-nil by the first cancel call
cause error // set to non-nil by the first cancel call
}
canceler 是一个接口,定义代码如下所示:
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err, cause error)
Done() <-chan struct{}
}
一个类型要想实现接口 canceler 就必须实现两个函数 cancel 和 Done。当我们执行 cancel 方法时,会发生什么呢?ctx2又会发生什么呢?
-
mutex将会被上锁,其他的goroutine则无法修改这个context -
done将会被关闭 - 所有派生出来的context将会被关闭,例如 ctx2
-
mutex将会解锁
假设我们派生出更多的context,则会形成一个链式结构,示例代码如下所示:
func main() {
ctx1 := context.Background()
ctx2, c2 := context.WithCancel(ctx1)
ctx3, c3 := context.WithCancel(ctx2)
ctx4, c4 := context.WithCancel(ctx3)
}
当我们调用c2时,将会出取消ctx2,同时也会取消派生出来的 ctx3 ,取消 ctx3 时,也会取消其派生出来的 ctx4 ,最终形成的链式取消传播示意图如下所示:

24.4.7 defer cancel()
像下面的代码是非常常见的,如下所示:
ctx, cancel = context.WithCancel(ctx)
defer cancel()
上面这种写法在标准库或其他第三方库非常常见,只要我们派生出了context,cancel则会在defer中进行调用。为什么要显式调用 cancel 呢?在我们创建一个库时,无法确定其他调用者能否正确在一个父context中调用并执行cancel,因此,只要在 defer 中额外添加调用 cancel 就能保证其被正确调用执行。为了更好的理解这一现象,我们来看看 Goroutine 泄漏。
24.7.1 Goroutine 泄漏
我们先定义两个函数 doSth 和 doSth2,这两个函数均是一种模拟行为,其第一个参数是context类型,他们一直在等待通道返回 ctx.Done,示例代码如下所示:
func doSth(ctx context.Context) {
select {
case <-ctx.Done():
log.Println("first goroutine return")
return
}
}
func doSth2(ctx context.Context) {
select {
case <-ctx.Done():
log.Println("second goroutine return")
return
}
}
再使用第三个函数来调用函数 doSth 和 doSth2,调用代码如下所示:
func lanch() {
ctx:=context.Background()
ctx,_=context.WithCancel(ctx)
log.Println("lanch first goroutine")
go doSth(ctx)
log.Println("lanch second goroutine")
go doSth2(ctx)
}
在上述代码中,我们首先通过context.Background()创建了一个root context,再调用 WithCancel(ctx) 返回一个可以被取消的context。 再通过 main 函数来启动 goroutine,代码如下所示:
func main() {
log.Println("begin program")
go lanch()
time.Sleep(10*time.Millisecond)
log.Printf("Goroutine count %d\n",runtime.NumGoroutine())
for {
}
}
在程序启动之后,通过 runtime.NumGoroutine() 来获取 Goroutine 的数量,应该为3
- 1个main goroutine
- 1个执行 doSth 的 goroutine
- 1个执行 doSth2 的 goroutine
如果我们不调用 cancel ,则后面两个 goroutine 将会永久运行。注意这里,虽然在 main 函数中创建了运行 lanch 的 goroutine,但不会被统计,因为其差不多是瞬间就运行完成并返回。当我们执行取消context时,doSth 和 doSth2 两个 goroutine 会取消并返回,因此goroutine数量会减少到1(main函数中的goroutine),但在这个示例中,我们并没有取消context。运行结果如下所示:
$ go run main.go
2025/11/21 00:03:35 begin program
2025/11/21 00:03:35 lanch first goroutine
2025/11/21 00:03:35 lanch second goroutine
2025/11/21 00:03:35 Goroutine count 3
在 main 函数,我们没有办法取消 context,因为context被定义在其他函数的内部,这个时候就会存在两个 Goroutine 泄漏。解决这个问题,我们可以修改 lanch 的代码,并添加一个 defer(),代码如下所示:
func lanch() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
log.Println("lanch first goroutine")
go doSth(ctx)
log.Println("lanch second goroutine")
go doSth2(ctx)
}
修改代码后的运行结果如下所示:
$ go run main.go
2025/11/21 00:09:17 begin program
2025/11/21 00:09:17 lanch first goroutine
2025/11/21 00:09:17 lanch second goroutine
2025/11/21 00:09:17 first goroutine return
2025/11/21 00:09:17 second goroutine return
2025/11/21 00:09:17 Goroutine count 1
24.4.8 WithValue
Context 自身也是可以承载数据的,这个功能通常适用于带请求域的场景,如下所示:
- 需要使用凭证的场景,例如JWT(JSON Web Token)
- RequstID,例如在系统跟踪请求调用
- 请求IP
- 请求头信息,例如User-Agent
24.4.8.1 案例
示例如下所示:
package main
import (
"context"
"fmt"
"log"
"net/http"
uuid "github.com/satori/go.uuid"
)
type key int
const (
requestID key = iota
jwt
)
func main() {
http.HandleFunc("/status", status)
err := http.ListenAndServe(":9090", nil)
if err != nil {
log.Fatal(err)
}
}
func status(w http.ResponseWriter, r *http.Request) {
// 将 requestID 添加至 context
ctx := context.WithValue(r.Context(), requestID, uuid.NewV4().String())
// 添加 凭证 到 Context
r.Header.Set("Authorization", "Surpass-Authorization-Credentials")
ctx = context.WithValue(ctx, jwt, r.Header.Get("Authorization"))
upDb, err := isDatabaseUp(ctx)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
upAuth, err := isMonitoringUp(ctx)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
credentail, err := getCredential(ctx)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
fmt.Fprintf(w, "DB up %t , Monitoring up: %t , credentail is %s\n", upDb, upAuth, credentail)
}
func isDatabaseUp(ctx context.Context) (bool, error) {
// 查询 requestID 的值
reqID, ok := ctx.Value(requestID).(string)
if !ok {
return false, fmt.Errorf("request ID in context doesn't have the expected type")
}
log.Printf("req %s - checking db status\n", reqID)
return true, nil
}
func isMonitoringUp(ctx context.Context) (bool, error) {
// 查询 requestID 的值
reqID, ok := ctx.Value(requestID).(string)
if !ok {
return false, fmt.Errorf("requestID in context doesn't have the expected type")
}
log.Printf("req %s - checking monitoring status\n", reqID)
return true, nil
}
func getCredential(ctx context.Context) (string, error) {
credential, ok := ctx.Value(jwt).(string)
if !ok {
return "", fmt.Errorf("credentails in context doesn't have the expected type")
}
return credential, nil
}
代码功能解释如下所示:
- 创建HTTP Server 并监听在端口
9090 - HTTP Server 的路由为
/status - 创建派生请求context:
ctx := context.WithValue(r.Context(), requestID, uuid.NewV4().String()),并添加了一个key-value对到context中 - 再次创建派生请求context:
ctx = context.WithValue(ctx, jwt, r.Header.Get("Authorization")),敢添加了一个key-value对到context中 - 以上在context中添加的值可以在函数
isDatabaseUp和isMonitoringUp是可见的,因此也可以在这两个函数中进行查询
24.4.8.2 key 类型
以下为 WithValue 的函数签名定义,如下所示:
func WithValue(parent Context, key, val any) Context
参数 key 和 val 类型都是 any,即这两个参数的类型可以是任意类型。但需要遵循一个限制,即 key 类型需要为 comparable。除此之外,也需要注意以下几点
- 可以在多个包之间共享一个 context
- 如果不想在多个包之间共享 context 中的值,可以将类型设置为不可导出类型,即以小写字母开头进行命名
- 可以在一个包里面定义全局的key
type key int
const (
requestID key = iota
jwt
)
当
key-value对在context中未获取到值时,通常会返回nil,这也是在代码要进行类型断言的原因。
24.5 总结
-
context是Go里面的一个标准库 -
context适用于以下场景
- 1.取消传播,例如,客户端取消请求后,可以用来取消一个请求链
- 2.在一个请求域内传输数据
- 3.设定截止时间或超时时间
- 截止时间:表示一个确切的时间点
- 超时时间:表示一段持续时间
- 在一个包内部,context 会被构造为一个链表
- 创建空的 context ,可以使用
Background
ctx := context.Background()
- 基于空 context 派生出以下几种 context
- WithCancel:
ctx, cancel := context.WithCancel(context.Background())- WithTimeout:
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)- WithDeadline:
ctx, cancel := context.WithDeadline(context.Background(), deadline)- WithValue:
ctx := context.WithValue(context.Background(),"key","value")
- 在父级 context 取消后,同时也会取消其派生出来的子 context
- context 可以在请求域内携带传输数据