golang Context介绍

Context(上下文) 是 Go 语言标准库中的一个核心工具,主要用于:

控制 Goroutine 的生命周期(取消、超时)

在 Goroutine 之间传递请求级别的元数据(如请求ID、用户信息等)

Context 到底怎么用?

想象一个场景:

你(主 goroutine)安排 3 个同事(子 goroutine)一起做一个"整理文件"的任务,要求:

  1. 如果任务中途不需要做了(比如老板说不整理了),你要能通知所有同事停下,别白干活;
  2. 如果整理时间超过 5 分钟,不管有没有做完,都必须停下;
  3. 你还想告诉同事们"整理的是 2026 年的文件"这个关键信息。

Context 就是干这个的:它是 goroutine 之间的"通信员",负责传递"取消信号""超时时间""共享数据",让多个 goroutine 协同工作、优雅退出

核心:Context 的 4 个基础用法

Context 本身是一个接口,但我们不用自己实现,Go 提供了 4 个最常用的"创建函数",只要掌握这 4 个就够了:

  1. context.Background():根 Context,所有 Context 的"祖宗",无超时、不可取消、无数据;
  2. context.WithCancel(parent):基于父 Context 创建"可手动取消"的 Context;
  3. context.WithTimeout(parent, 超时时间):基于父 Context 创建"超时自动取消"的 Context;
  4. context.WithValue(parent, key, value):基于父 Context 创建"带共享数据"的 Context。

实例 1:手动取消 Context(通知 goroutine 停止工作)

需求:主 goroutine 启动 2 个子 goroutine 模拟"干活",3 秒后手动通知它们停止。

go 复制代码
package main

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

// 子goroutine干活的函数
func work(ctx context.Context, name string) {
	// 循环干活,直到收到取消信号
	for {
		select {
		// 监听Context的取消信号(ctx.Done()通道关闭就触发)
		case <-ctx.Done():
			fmt.Printf("【%s】收到停止信号,停止干活!\n", name)
			return // 优雅退出
		default:
			// 没收到信号,继续干活
			fmt.Printf("【%s】正在干活...\n", name)
			time.Sleep(500 * time.Millisecond) // 干0.5秒歇一下
		}
	}
}

func main() {
	// 1. 创建根Context(background是所有Context的基础)
	rootCtx := context.Background()

	// 2. 创建可手动取消的Context,返回:子Context + 取消函数
	ctx, cancel := context.WithCancel(rootCtx)
	defer cancel() // 好习惯:确保函数结束时取消,避免泄露

	// 3. 启动2个子goroutine干活
	go work(ctx, "同事1")
	go work(ctx, "同事2")

	// 4. 主goroutine等3秒,然后通知子goroutine停止
	time.Sleep(3 * time.Second)
	fmt.Println("主goroutine:老板说不用干了,通知所有人停下!")
	cancel() // 手动触发取消

	// 等子goroutine退出(可忽略,只是为了看输出)
	time.Sleep(1 * time.Second)
	fmt.Println("所有任务结束")
}
运行结果:
复制代码
【同事1】正在干活...
【同事2】正在干活...
...(循环3秒)
主goroutine:老板说不用干了,通知所有人停下!
【同事1】收到停止信号,停止干活!
【同事2】收到停止信号,停止干活!
所有任务结束
关键理解:
  • cancel() 是"开关":调用它,ctx.Done() 通道就会关闭,子 goroutine 能监听到;
  • select + <-ctx.Done() 是"监听开关":子 goroutine 靠这个知道要不要停;
  • defer cancel():防止主 goroutine 提前退出,导致 Context 没取消,子 goroutine 一直跑(goroutine 泄露)。

实例 2:超时自动取消 Context(不用手动关,到点就停)

需求:子 goroutine 干活,但最多干 2 秒,超时自动停止(比如 DNS 解析超时)。

go 复制代码
package main

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

func workWithTimeout(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			// ctx.Err() 能看取消原因:超时/手动取消
			fmt.Printf("干活停止,原因:%v\n", ctx.Err())
			return
		default:
			fmt.Println("正在干活...")
			time.Sleep(500 * time.Millisecond)
		}
	}
}

func main() {
	// 1. 创建带超时的Context:2秒后自动取消
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel() // 即使超时了,cancel()也能清理资源,好习惯

	// 2. 启动子goroutine
	go workWithTimeout(ctx)

	// 3. 主goroutine等3秒,看结果
	time.Sleep(3 * time.Second)
	fmt.Println("主goroutine:任务结束")
}
运行结果:
复制代码
正在干活...
正在干活...
正在干活...
正在干活...
干活停止,原因:context deadline exceeded
主goroutine:任务结束
关键理解:
  • WithTimeoutWithCancel 多了"自动触发":到时间不用手动调 cancel(),Context 会自己关闭 Done() 通道;
  • ctx.Err() 很有用:返回 context.DeadlineExceeded 表示超时,返回 context.Canceled 表示手动取消。

实例 3:带共享数据的 Context(传递关键信息)

需求:主 goroutine 给子 goroutine 传递"任务ID""用户ID"这类轻量数据(注意:只能传轻量数据,不能传大对象)。

go 复制代码
package main

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

// 定义key(推荐用自定义类型,避免和其他包的key冲突)
type ctxKey string

func workWithValue(ctx context.Context) {
	// 从Context中取值:key要和存的时候一致
	taskID := ctx.Value(ctxKey("taskID")).(string) // 类型断言(记得加.(类型))
	userID := ctx.Value(ctxKey("userID")).(int)

	fmt.Printf("收到任务信息:taskID=%s, userID=%d,开始干活...\n", taskID, userID)
	time.Sleep(1 * time.Second)
	fmt.Println("干活完成!")
}

func main() {
	// 1. 根Context
	rootCtx := context.Background()

	// 2. 第一层:加taskID
	ctxWithTask := context.WithValue(rootCtx, ctxKey("taskID"), "task_001")
	// 3. 第二层:再加userID(Context是链式的,可多层叠加)
	ctxWithUser := context.WithValue(ctxWithTask, ctxKey("userID"), 10086)

	// 4. 启动子goroutine,传递带数据的Context
	go workWithValue(ctxWithUser)

	// 等子goroutine完成
	time.Sleep(2 * time.Second)
	fmt.Println("主goroutine:任务结束")
}
运行结果:
复制代码
收到任务信息:taskID=task_001, userID=10086,开始干活...
干活完成!
主goroutine:任务结束
关键理解:
  • WithValue 是"链式叠加":可以基于一个 Context 不断加新数据,子 Context 能拿到所有父 Context 的数据;
  • 取数据要做类型断言ctx.Value(key).(类型),因为 Value() 返回的是 interface{}
  • 不要传大数据/频繁修改的数据:Context 设计目的是传递"请求级"的轻量元数据(比如请求ID、用户ID),不是用来做数据共享的"容器"。

实例 4:综合案例(结合取消 + 超时 + 传数据)

模拟之前的 DNS 解析场景:启动子 goroutine 解析 DNS,传递"域名"信息,设置 3 秒超时,若手动取消则立即停止。

go 复制代码
package main

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

// 模拟DNS解析
func resolveDNS(ctx context.Context) {
	// 取传递的域名
	domain := ctx.Value(ctxKey("domain")).(string)
	fmt.Printf("开始解析域名:%s\n", domain)

	for {
		select {
		case <-ctx.Done():
			fmt.Printf("解析停止,原因:%v\n", ctx.Err())
			return
		default:
			fmt.Println("解析中...")
			time.Sleep(500 * time.Millisecond)
		}
	}
}

type ctxKey string

func main() {
	// 1. 根Context + 传域名 + 3秒超时
	rootCtx := context.Background()
	ctxWithDomain := context.WithValue(rootCtx, ctxKey("domain"), "dnspod.mymei.tv")
	ctx, cancel := context.WithTimeout(ctxWithDomain, 3*time.Second)
	defer cancel()

	// 2. 启动解析goroutine
	go resolveDNS(ctx)

	// 3. 模拟2秒后手动取消(注释这行就是超时取消)
	// time.Sleep(2 * time.Second)
	// fmt.Println("手动取消解析!")
	// cancel()

	// 等结果
	time.Sleep(4 * time.Second)
	fmt.Println("主程序结束")
}
运行结果(不手动取消,超时):
复制代码
开始解析域名:dnspod.mymei.tv
解析中...
解析中...
解析中...
解析中...
解析中...
解析中...
解析停止,原因:context deadline exceeded
主程序结束
运行结果(手动取消):
复制代码
开始解析域名:dnspod.mymei.tv
解析中...
解析中...
解析中...
手动取消解析!
解析停止,原因:context canceled
主程序结束

总结(3 个关键点)

  1. 核心作用:Context 是 goroutine 之间的"通信员",主要用来传递「取消信号」「超时时间」「轻量共享数据」;
  2. 3 个常用创建方式
    • WithCancel:手动取消,适合"主动叫停任务";
    • WithTimeout:超时自动取消,适合"有时间限制的任务"(如网络请求、DNS解析);
    • WithValue:传递轻量元数据,适合"请求级数据传递"(如请求ID、用户ID);
  3. 核心用法 :子 goroutine 用 select <-ctx.Done() 监听取消信号,收到后优雅退出,避免 goroutine 泄露。
相关推荐
_OP_CHEN1 小时前
【算法提高篇】(四)线段树之多个区间操作:懒标记优先级博弈与实战突破
算法·蓝桥杯·线段树·c/c++·区间查询·acm、icpc·区间操作
俩娃妈教编程1 小时前
2025 年 09 月 三级真题(1)--数组清零
c++·算法·gesp真题
大黄说说2 小时前
Spring Boot 3 新特性详解与迁移指南:从 Java 17 到云原生最佳实践
开发语言·python
AI科技星2 小时前
时空的几何动力学:基于光速螺旋运动公设的速度上限定理求导与全维度验证
人工智能·线性代数·算法·机器学习·平面
㓗冽2 小时前
进制转换(字符串)-基础题82th + 表达式求值(字符串)-基础题83th + 删除字符(字符串)-基础题84th
算法
小范自学编程2 小时前
算法训练营 Day31 - 贪心算法 Part05
算法·贪心算法
锅包一切2 小时前
PART2 双指针
c++·算法·leetcode·力扣·双指针
tankeven2 小时前
HJ91 走方格的方案数
c++·算法
俩娃妈教编程2 小时前
2024 年 09 月 二级真题(2)--小杨的矩阵
c++·算法·gesp真题