Go Context 到底是什么?

Context 是 Go 中用来控制"一次任务生命周期"的工具。

可以用于取消任务、设置超时时间和在一次请求链路中传递一些上下文信息。

我们可以把 Context 想象成一个"遥控器"。

一个 HTTP 请求进来了:

text 复制代码
用户请求
   │
   ▼
HTTP Handler
   │
   ├── 查询用户
   │
   ├── 查询订单
   │
   └── 调用 AI 服务

如果用户突然关闭页面,我们希望后面的工作也别干了,以免浪费系统资源。

这时候,Context 就可以按下取消按钮,停止后续的任务。

Context 的三个功能

学习 Context 时,记住这三个词:

text 复制代码
Context
  │
  ├── Cancel      取消
  │
  ├── Deadline    截止时间
  │
  └── Value       上下文数据

这三个词对应 Context 的类型。

Cancel

告诉任务:

别干了。

Deadline / Timeout

告诉任务:

最多干这么久。

Value

告诉下游:

这是这次请求携带的一些上下文信息。

案例一:手动取消任务

我们来看一个取消任务的例子:

go 复制代码
package main

import (
    "context"
    "fmt"
    "time"
)

func work(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("任务被取消")
            return

        default:
            fmt.Println("正在工作...")
            time.Sleep(time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go work(ctx)

    time.Sleep(3 * time.Second)

    cancel()

    time.Sleep(time.Second)
}

运行结果:

text 复制代码
正在工作...
正在工作...
正在工作...
任务被取消

context.WithCancel() 创建一个带有取消功能的上下文。

go 复制代码
ctx, cancel := context.WithCancel(context.Background())

ctx.Done() 会返回一个 Channel ,当 cancel() 被调用时,这个 Channel 就会被关闭,从而实现通知任务取消。

go 复制代码
select {
case <-ctx.Done():
    return
}

案例二:设置任务超时时间

我们来看使用 Context 设置任务超时时间的例子

go 复制代码
func work(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("任务结束:", ctx.Err())
            return

        default:
            fmt.Println("正在工作...")
            time.Sleep(time.Second)
        }
    }
}

然后:

go 复制代码
func main() {
    ctx, cancel := context.WithTimeout(
        context.Background(),
        3*time.Second,
    )

    defer cancel()

    work(ctx)
}

大约 3 秒之后:

text 复制代码
正在工作...
正在工作...
正在工作...
任务结束: context deadline exceeded

这里:

go 复制代码
ctx.Err()

可以告诉我们 Context 为什么结束。

常见的结果包括:

go 复制代码
context.Canceled

表示:

被主动取消了。

以及:

go 复制代码
context.DeadlineExceeded

表示:

超时了。

案例三:传递上下文数据

我们来实现一个使用 Context 传递上下文数据的例子。

go 复制代码
package main

import (
	"context"
	"fmt"
)

type ctxKey string // 自定义 key 类型,避免和别人的 key 撞车

const traceIDKey ctxKey = "traceID"

func withTraceID(ctx context.Context, id string) context.Context {
	return context.WithValue(ctx, traceIDKey, id)
}

func handle(ctx context.Context) {
	if id, ok := ctx.Value(traceIDKey).(string); ok {
		fmt.Println("traceID =", id)
	}
}

func main() {
	ctx := withTraceID(context.Background(), "req-42")
	handle(ctx) // → traceID = req-42
}

运行结果:

text 复制代码
traceID = req-42

注意 Context.Value 不是什么都能放,比如业务参数 、大型对象和普通函数参数就不适合放在 Context.Value 中。

Context 的 Value 适合传递 trace ID 、鉴权相关的请求上下文等请求域等数据。

key 一定要用自定义类型 ,不要用裸 string

go 复制代码
ctx = context.WithValue(ctx, "traceID", id)   // ✗ 别的包可能也用 "traceID"
ctx = context.WithValue(ctx, traceIDKey, id)  // ✓ 类型不导出,天然隔离

取出来必须做类型断言,并且不要存指针指向可变对象------Context 是并发安全的,但它存的值本身不会帮你加锁:

go 复制代码
v := ctx.Value(traceIDKey)
id, ok := v.(string) // 一定要用带 ok 的形式,否则可能 panic

Context 可以一层一层传递

Context 厉害的地方,是它可以沿着调用链一路传递。

例如一个 HTTP 请求:

text 复制代码
HTTP Request
     │
     ▼
Handler
     │
     ▼
Service
     │
     ▼
Repository
     │
     ▼
Database

我们可以让 Context 一路传下去:

go 复制代码
func Handler(ctx context.Context) {
    user := GetUser(ctx)
}

func GetUser(ctx context.Context) *User {
    return queryDatabase(ctx)
}

func queryDatabase(ctx context.Context) *User {
    // 使用 ctx 查询数据库
}

于是:

text 复制代码
Handler
   │
   │ ctx
   ▼
Service
   │
   │ ctx
   ▼
Repository
   │
   │ ctx
   ▼
Database

如果最上面的请求取消:

text 复制代码
HTTP 请求取消
      │
      ▼
    ctx
      │
      ├── Handler
      │
      ├── Service
      │
      └── Database

下游都可以感知到,类似于"遥控器"。

Context 的"签名约定"

Go 社区强制约定:

go 复制代码
func DoSomething(ctx context.Context, a int, b string) error {
	// ctx 永远是第一个参数,命名永远叫 ctx
}

这样一眼就能看出来:

这个函数支持 Context 控制。

Context 最常见的场景:HTTP 服务

在写 Go Web 服务的时候,我们一般都会使用 Context。

例如:

go 复制代码
func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    user, err := GetUser(ctx, 1001)

    if err != nil {
        return
    }

    fmt.Fprintln(w, user)
}

这里:

go 复制代码
r.Context()

就是当前 HTTP 请求对应的 Context。

你可以理解成:

text 复制代码
浏览器
  │
  │ HTTP Request
  ▼
Go HTTP Server
  │
  ▼
Request Context
  │
  ├── Service
  ├── Database
  └── Third-party API

因此,在 Web 服务中:

HTTP 请求的 Context 通常是整条调用链的"总指挥"。

Context + 数据库

数据库操作同样支持 Context。

例如:

go 复制代码
rows, err := db.QueryContext(
    ctx,
    "SELECT * FROM users WHERE id = ?",
    userID,
)

这里:

go 复制代码
QueryContext(...)

意味着:

这个数据库查询属于这个 Context。

如果 Context 被取消,数据库驱动就有机会终止这个查询。

所以实际项目中经常看到:

go 复制代码
func GetUser(ctx context.Context, id int) (*User, error) {
    return db.QueryContext(
        ctx,
        "SELECT * FROM users WHERE id = ?",
        id,
    )
}

Context 是树结构

Context 还有一个非常重要的特点:

Context 是可以派生的。

例如:

go 复制代码
root := context.Background()

ctx1, cancel1 := context.WithCancel(root)

ctx2, cancel2 := context.WithTimeout(
    ctx1,
    5*time.Second,
)

可以画成:

text 复制代码
             root
              │
              ▼
            ctx1
              │
              ▼
            ctx2

如果:

go 复制代码
cancel1()

那么:

text 复制代码
ctx1 ❌
 │
 └── ctx2 ❌

父 Context 被取消:

子 Context 也会被取消。

但是反过来:

go 复制代码
cancel2()

只会影响:

text 复制代码
ctx2 ❌

不会影响:

text 复制代码
ctx1 ✅

所以 Context 的关系可以理解为:

父 Context 控制子 Context 的生命周期。

这也是为什么 Context 特别适合请求链路,例如:

text 复制代码
HTTP Request Context
        │
        ├── User Service
        │      │
        │      └── Database
        │
        ├── Order Service
        │      │
        │      └── Database
        │
        └── AI Service
               │
               └── HTTP API

整个请求取消:

text 复制代码
HTTP Request ❌
      │
      ├── User Service ❌
      ├── Order Service ❌
      └── AI Service ❌

这就形成了一个完整的生命周期控制体系。

context.Background() 是什么?

你经常会看到:

go 复制代码
context.Background()

它是一个最基础的 Context。

可以把它理解成:

Context 世界的根节点。 ------一个永远不取消、没有截止时间、不携带任何值的空 ctx,只该出现在 main、初始化、测试这类调用链的起点。

例如:

go 复制代码
ctx := context.Background()

然后:

go 复制代码
ctx, cancel := context.WithCancel(ctx)

再:

go 复制代码
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)

context.TODO() 是什么?

context.TODO() 的行为和 context.Background() 完全一样(永不取消、无超时、无值),差别只在语义 ------它是一句"我知道这里该有一个真正的 ctx,但暂时还没想好传哪个"的待办标记。

例如开发过程中:

go 复制代码
func DoSomething(ctx context.Context) {
    // ...
}

但调用方暂时没有 Context:

go 复制代码
DoSomething(context.TODO())

可以先这样写。

不过在明确知道应该使用哪个 Context 时,优先使用真实的 Context。

又假设我们正在重构一个老项目,原来的代码没有 Context:

go 复制代码
func GetUser(userID int) {
	fmt.Println("查询用户:", userID)
}

现在我们希望让它支持 Context:

go 复制代码
func GetUser(ctx context.Context, userID int) {
	fmt.Println("查询用户:", userID)
}

但是项目中还有很多地方调用:

go 复制代码
GetUser(1001)

暂时还没来得及把真正的 Context 传下来。

这时候可以临时改成:

go 复制代码
GetUser(context.TODO(), 1001)

等以后把调用链改造完成,再替换成真正的 Context:

go 复制代码
func Handler(w http.ResponseWriter, r *http.Request) {
	ctx := r.Context()

	GetUser(ctx, 1001)
}

defer cancel() 为什么到处都是?

我们经常会看到:

go 复制代码
ctx, cancel := context.WithTimeout(
    context.Background(),
    5*time.Second,
)

defer cancel()

可能有人会问:

"不是 5 秒之后自动取消吗?为什么还要 cancel()?"

因为:

go 复制代码
defer cancel()

可以确保:

函数提前结束时,及时释放 Context 相关资源。

所以这是一个非常常见的 Go 写法:

go 复制代码
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

记住这个模式即可。

总结

如果用一句话解释 Context:

Context 是 Go 用来管理一次请求或任务生命周期的机制,可以让调用链中的函数共享取消信号、超时信息和少量请求范围的数据。

再简单一点:

Context 就是一个贯穿整个调用链的"任务遥控器"。

它主要负责三件事:

text 复制代码
取消
超时
传递请求上下文

用一个接近生活的案例来类比:

假设你用手机在餐厅点了一份外卖。

text 复制代码
手机
 │
 ▼
餐厅
 │
 ▼
厨师
 │
 ▼
配菜
 │
 ▼
外卖员

订单就是一次请求。

Context 就像订单上的一个"状态控制器"。

如果你在手机上取消订单:

text 复制代码
订单取消
   │
   ├── 厨师:别做了
   ├── 配菜:别准备了
   └── 外卖员:别送了

这就是:

go 复制代码
cancel()

如果订单超 30 分钟后自动取消订单,这就是:

go 复制代码
context.WithTimeout(ctx, 30*time.Minute)

如果订单上还有:

text 复制代码
订单号:A10086
用户:张三
TraceID:abc123

这些属于这次订单的上下文信息,可以类比:

go 复制代码
context.WithValue(...)

所以:

Context 不是用来"做事情"的,而是用来管理事情怎么结束、什么时候结束,以及在这条调用链上携带哪些上下文信息。

相关推荐
摇滚侠1 小时前
《Spring Boot 3:高级与架构设计》第 2 章 IOC容器的高级机制 BeanFactoryPostProcessor 个人理解 6
java·spring boot·笔记·后端
苏三说技术1 小时前
推荐一个牛逼的AgentScope系统
后端
行百里er1 小时前
5 分钟跑起 Redis(Docker 版)
redis·后端·docker
程序员阿鹏1 小时前
简单介绍一下软引用
java·开发语言·jvm·数据结构·后端
回家路上绕了弯2 小时前
如何提升 AI 编码准确性:从“生成代码”到“验证结果”
后端
newerp2 小时前
系统调用与 M/P 解绑
后端·程序员·go
wno7042 小时前
Spring Security Session管理
java·后端·spring
吃饱了得干活2 小时前
主键选择:从单机到分布式,一个被低估的性能决策
java·后端
PC2005_cloud2 小时前
Windows 数据使用量页面卡死:1097 个热点档案和 161 MB 的 SRUM 库
前端·后端