24.Context

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 取消订单,则可以通过自顶向下,逐层传递取消订单的信息。如下所示:

2401-Context取消传播示意图.png

    在人类的日常活动中,我们总有一种方式来通知各种突发情况而导致取消的事情。而在计算机科学里面,我们也可以借鉴这种取消传播的方式,从而引入context 机制。比如,当我们发送一个HTTP请求之后,而在客户端取消连接之后,服务端也可以取消后面所需要各种调用链。

24.2.2 请求域内传输数据

    当一个请求被发送至服务器之后,Web Server 中的响应函数并不是单独工作,这个响应函数可能还调用了其他函数,而其他函数又可能调用了其他的函数,从而形成一个调用链。在微服务模式中,发生一个请求之后,会产生一个新的请求,而这个新的请求又会调用另一个微服务的请求等等,从而形成一个调用链,称之为调用栈。在这里,我们将会理解为什么在调用栈之间传输数据是非常有用的。

    以下日常购物为例。一般将会经历以下步骤:

  • 用户通过浏览器或APP登录
  • 填写登录信息
  • 向服务端发送鉴权信息,服务端调用鉴权服务
  • 服务端构建个人账户页面(例如通过template)并发送到用户端
  • 如果用户查询最近订单,服务端将调用订单服务查询并返回给客户端

    以上整个流程图示例如下所示:

2402-Context购物HTTP请求流程图.png

    以上流程图中,哪些信息可以添加到 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 等等。链表中元素称之为节点,每一个节点包含数据域指针域,其中指针域中存储的是下一个节点的内存地址。

    链表具备以下特性

  • 第一个节点称之为头部节点,最后一个节点称这为尾部节点
  • 除头部节点和尾部节点外,每个节点都会有一个子节点和父节点

    示意图如下所示:

2403-Context链表示意图-1.png

2404-Context链表示意图-2.png

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。如下所示:

2405-Context派生.png

    基于 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 ,其中 CancelFunccontext 包中自定义的类型,,其定义如下所示:

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() 传入两个参数 rootCtx50*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密集型 任务一个示例,并无太多业务逻辑,其工作流程如下所示:

2406-Context-CPU计算密集型工作流程图.png

    函数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 的执行流程如下所示:

2407-Context-doWork执行流.png

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

    WithDeadlineWithTimeout 非常类似。如果我们去看 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 和 一个指定的时间。正如前面所言,截止时间和超时时间两个非常类似

  • 超时时间表示一段持续时间
  • 截止时间表示一个特定的时间节点

    两者比较示意图如下所示:

2408-Context-截止时间和超时时间区别.png

    基于上述比较,在有些情况中,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 就必须实现两个函数 cancelDone。当我们执行 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 ,最终形成的链式取消传播示意图如下所示:

2409-Context链式取消示意图.png

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 泄漏

    我们先定义两个函数 doSthdoSth2,这两个函数均是一种模拟行为,其第一个参数是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
    }
}

    再使用第三个函数来调用函数 doSthdoSth2,调用代码如下所示:

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时,doSthdoSth2 两个 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中添加的值可以在函数isDatabaseUpisMonitoringUp是可见的,因此也可以在这两个函数中进行查询

24.4.8.2 key 类型

    以下为 WithValue 的函数签名定义,如下所示:

func WithValue(parent Context, key, val any) Context 

    参数 keyval 类型都是 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 可以在请求域内携带传输数据
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容