Skip to content
鼓励作者:欢迎打赏犒劳

context.Context是什么?

ctx.Done() 通道 说明书上的“取消信号”指示灯

至于web框架hertz,接口结束了,异步方法用到的ctx到底会不会取消呢?我感觉应该是会取消的,但是在我的本地测试的情况是并没有取消。 我的环境是window,不知道是不是环境的原因。不管了,小问题

你不能直接创建 Context,但可以通过 context.Background() 派生出新的 context:

go
// 1. 基于 Background 创建可取消的 context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// 2. 带超时的 context(推荐用于 HTTP 请求)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

// 3. 带截止时间的 context
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Second))
defer cancel()

// 4. 带值的 context(用于传递请求数据)
ctx := context.WithValue(context.Background(), "user_id", "123")

核心比喻:快递配送任务(Go Routine)和它的“任务说明书”(Context)

想象一下,你是一个仓库管理员(主 Goroutine,比如一个 HTTP 服务器),你需要派一个快递员(一个子 Goroutine)去送一个包裹。

为了让快递员高效、安全地工作,你不会只告诉他地址就完事了。你会给他一张 “任务说明书”,这张说明书就是 context.Context

这张说明书可以包含多种关键信息:


1. context.Background() - 空白任务书

这是所有说明书的底版。就像一张空白的、盖了公章的派遣单。它本身不包含任何具体指令,但你可以基于它来创建各种具体的说明书。

go
ctx := context.Background() // 拿到一张空白的派遣单

2. context.WithCancel() - 可随时取消的任务

  1. 取消是协作式的。(Goroutine 必须主动检查 ctx.Done(),不能强制杀死goroutine。这是一种"礼貌的请求停止"而不是"强制终止"。)
  2. 一个Cancel函数控制多个Goroutine。(这是最强大的特性!你不需要维护每个goroutine的引用,只需要调用一次 cancel(),所有监听这个context的goroutine都会收到信号。)

你告诉快递员:“这是地址,开始送吧。但是,如果我给你打信号(比如对讲机里喊‘取消’),你必须立刻停止配送,马上回来!

  • Go 代码
    go
    ctx, cancel := context.WithCancel(context.Background())
    go deliverPackage(ctx) // 派快递员出发,并把“可取消说明书”给他
    // ... 过了一会,客户取消订单了
    cancel() // 你拿起对讲机喊“取消任务!”
    deliverPackage 函数里,快递员需要不断地检查说明书:select { case <-ctx.Done(): return }。一旦收到 cancel 信号,ctx.Done() 这个通道就会关闭,快递员就知道该返回了。

完整的示例demo:

go
package main

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

// 模拟一个耗时的任务,比如处理数据、调用API等
func longRunningTask(ctx context.Context, taskName string) {
	for i := 1; i <= 10; i++ {
		// 关键:在每次循环开始时检查context是否被取消
		select {
		case <-ctx.Done():  // 当cancel()被调用时,这个case会被触发
			// 如果context被取消,就停止任务
			fmt.Printf("【%s】任务被取消啦!原因:%v\n", taskName, ctx.Err())
			return
		default:
			// 如果没有被取消,就继续执行任务
			fmt.Printf("【%s】正在执行第 %d/10 步...\n", taskName, i)
			time.Sleep(500 * time.Millisecond) // 模拟耗时操作
		}
	}
	fmt.Printf("【%s】任务完成!\n", taskName)
}

func main() {
	fmt.Println("=== 场景1:正常完成任务 ===")
	demoNormalCompletion()

	fmt.Println("\n=== 场景2:中途取消任务 ===")
	demoCancellation()

	fmt.Println("\n=== 场景3:多个goroutine共享同一个context ===")
	demoMultipleGoroutines()
}

// 场景1:让任务正常完成
func demoNormalCompletion() {
	// 创建可取消的context
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel() // 确保函数退出时释放资源

	fmt.Println("启动任务,3秒后任务会自动完成...")
	go longRunningTask(ctx, "正常任务")

	// 等待足够长的时间让任务完成
	time.Sleep(6 * time.Second)
	fmt.Println("场景1结束")
}

// 场景2:中途取消任务
func demoCancellation() {
	ctx, cancel := context.WithCancel(context.Background())

	fmt.Println("启动任务,但2秒后会取消它...")
	go longRunningTask(ctx, "可取消任务")

	// 2秒后取消任务
	time.AfterFunc(2*time.Second, func() {
		fmt.Println("主程序发出取消信号!")
		cancel() // 这里调用cancel()来取消context
	})

	// 等待一段时间观察结果
	time.Sleep(4 * time.Second)
	fmt.Println("场景2结束")
}

// 场景3:多个goroutine共享同一个context
func demoMultipleGoroutines() {
	ctx, cancel := context.WithCancel(context.Background())

	// 启动3个worker
	for i := 1; i <= 3; i++ {
		go func(workerID int) {
			workerTask(ctx, workerID)
		}(i)
	}

	fmt.Println("3个worker已启动,3秒后全部取消...")
	time.Sleep(3 * time.Second)

	fmt.Println("取消所有worker!")
	cancel() // 一个cancel()调用可以取消所有相关的goroutine

	time.Sleep(1 * time.Second)
	fmt.Println("场景3结束")
}

// Worker任务,会持续运行直到被取消
func workerTask(ctx context.Context, workerID int) {
	for {
		select {
		case <-ctx.Done():
			fmt.Printf("Worker %d 收到取消信号,停止工作\n", workerID)
			return
		default:
			fmt.Printf("Worker %d 正在工作...\n", workerID)
			time.Sleep(1 * time.Second)
		}
	}
}

/**
=== 场景1:正常完成任务 ===
启动任务,3秒后任务会自动完成...
【正常任务】正在执行第 1/10 步...
【正常任务】正在执行第 2/10 步...
【正常任务】正在执行第 3/10 步...
【正常任务】正在执行第 4/10 步...
【正常任务】正在执行第 5/10 步...
【正常任务】正在执行第 6/10 步...
【正常任务】正在执行第 7/10 步...
【正常任务】正在执行第 8/10 步...
【正常任务】正在执行第 9/10 步...
【正常任务】正在执行第 10/10 步...
【正常任务】任务完成!
场景1结束

=== 场景2:中途取消任务 ===
启动任务,但2秒后会取消它...
【可取消任务】正在执行第 1/10 步...
【可取消任务】正在执行第 2/10 步...
【可取消任务】正在执行第 3/10 步...
【可取消任务】正在执行第 4/10 步...
主程序发出取消信号!
【可取消任务】任务被取消啦!原因:context canceled
场景2结束

=== 场景3:多个goroutine共享同一个context ===
3个worker已启动,3秒后全部取消...
Worker 2 正在工作...
Worker 1 正在工作...
Worker 3 正在工作...
Worker 3 正在工作...
Worker 1 正在工作...
Worker 2 正在工作...
Worker 3 正在工作...
Worker 2 正在工作...
Worker 1 正在工作...
取消所有worker!
Worker 3 收到取消信号,停止工作
Worker 2 收到取消信号,停止工作
Worker 1 收到取消信号,停止工作
场景3结束
*/

3. context.WithTimeout()context.WithDeadline() - 带时间限制的任务

你告诉快递员:“你必须在下单后 2小时内 送到(Timeout),否则算超时失败!” 或者 “你必须在 今天下午5点前 送到(Deadline),否则仓库关门了!”

  • Go 代码
    go
    // 方式一:2小时超时
    ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Hour)
    defer cancel() // 无论任务成功还是超时,都释放资源
    
    // 方式二:下午5点截止
    deadline := time.Date(2023, 10, 27, 17, 0, 0, 0, time.Local)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()
    
    go deliverPackage(ctx)
    快递员的工作方式和上面一样,他会监听说明书。时间一到,说明书会自动发出“取消”信号,不需要你手动打电话。这对于防止 HTTP 请求等操作无限期挂起至关重要。

context.WithTimeout

go
package main

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

func apiCall(ctx context.Context) error {
	select {
	case <-time.After(3 * time.Second):
		return nil // 正常完成
	case <-ctx.Done():
		return fmt.Errorf("请求被取消: %v", ctx.Err())
	}
}

func main() {
	// HTTP请求设置1秒超时
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()

	if err := apiCall(ctx); err != nil {
		fmt.Println("错误:", err) // 会输出这里
	} else {
		fmt.Println("请求成功")
	}
}
// 错误: 请求被取消: context deadline exceeded

context.WithDeadline

go
package main

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

func main() {
	// 设置具体的截止时间(3秒后)
	deadline := time.Now().Add(3 * time.Second)
	ctx, cancel := context.WithDeadline(context.Background(), deadline)
	defer cancel()

	// 模拟需要5秒的操作
	select {
	case <-time.After(5 * time.Second):
		fmt.Println("长时间操作完成")
	case <-ctx.Done():
		fmt.Println("到达截止时间:", ctx.Err()) // 3秒后会执行这里
	}
}
// 到达截止时间: context deadline exceeded

4. context.WithValue() - 携带额外信息的任务

你告诉快递员:“这个包裹是易碎品(fragile=true),收件人王太太脾气不好(attitude=grumpy),这些信息你路上看看,处理包裹时小心点。” 这些信息不是配送指令,而是和这次特定任务相关的元数据

  • Java 类比:有点像把数据放入 ThreadLocal,但它是显式传递的、更安全的。或者在 Web 框架中,类似于 HttpServletRequest 的 attribute,用于在请求处理链中传递用户信息、认证令牌等。
  • Go 代码
    go
    // 把用户ID和认证令牌附加到说明书上
    ctx := context.WithValue(context.Background(), "userID", 12345)
    ctx = context.WithValue(ctx, "authToken", "abc123")
    
    go deliverPackage(ctx)
    deliverPackage 内部,或者更深层的函数(比如需要认证的函数)里,可以读取这些值:
    go
    token := ctx.Value("authToken")

如有转载或 CV 的请标注本站原文地址