Go语言的context

article/2025/6/22 1:19:32

Golang context 实现原理

本篇文章是基于小徐先生的文章的修改和个人注解,要查看原文可以点击上述的链接查看

目前我这篇文章的go语言版本是1.24.1

context上下文

context被当作第一个参数(官方建议),并且不断的传递下去,基本上一个项目代码到处都是context,但是你们真的知道他有什么作用吗?

接下来就来看看这个golang世界中的典型工具吧

func main()  {ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second)defer cancel()go Monitor(ctx)time.Sleep(20 * time.Second)
}func Monitor(ctx context.Context)  {for {fmt.Print("monitor")}
}

但是他到底时如何处理并发控制和实现呢?

接下来就来深入看看他的原理和使用

一.context包介绍

context可以用来在goroutine之间传递上下文信息,相同的context可以传递给运行在不同goroutine中的函数,上下文对于多个goroutine同时使用是安全的,context包定义了上下文类型,可以使用backgroundTODO创建一个上下文,在函数调用链之间传播context,也可以使用WithDeadlineWithTimeoutWithCancelWithValue 创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:context的作用就是在不同的goroutine之间同步请求特定的数据、取消信号以及处理请求的截止日期。

目前我们常用的一些库都是支持context的,例如gindatabase/sql等库都是支持context的,这样更方便我们做并发控制了,只要在服务器入口创建一个context上下文,不断透传下去即可。

二.context的使用

2.1 context.Context

他是核心数据结构,看一下它的样子吧:

type Context interface {Deadline() (deadline time.Time, ok bool)Done() <-chan struct{}Err() errorValue(key any) any
}

Context 为 interface,定义了四个核心 api:

• Deadline:返回 context 的过期时间

• Done:返回 context 中的 channel

• Err:返回错误

• Value:返回 context 中的对应 key 的值

2.2 标准error

var Canceled = errors.New("context canceled")var DeadlineExceeded error = deadlineExceededError{}type deadlineExceededError struct{}func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool   { return true }
func (deadlineExceededError) Temporary() bool { return true }

• Canceled:context 被 cancel 时会报此错误

• DeadlineExceeded:context 超时时会报此错误

三.类的实现

3.1 emptyCtx

通过看它的源码我们会发现,这个空的上下文其实就是实现了这个context这个接口,对于实现的4个方法都是返回的nil,这就是为什么说他是empty

在之前的版本中他可能是int类型,后来已经被修改了

type emptyCtx struct{}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
}

• emptyCtx 是一个空的 context

• Deadline 方法会返回一个公元元年时间以及 false 的 flag,标识当前 context 不存在过期时间;

• Done 方法返回一个 nil 值,用户无论往 nil 中写入或者读取数据,均会陷入阻塞;

• Err 方法返回的错误永远为 nil;

• Value 方法返回的 value 同样永远为 nil.

3.1.1 context.Background() & context.TODO()

context包主要提供了两种方式创建context:

  • context.Backgroud()
  • context.TODO()

我们在看代码的时候,经常可以看到不是Background就是todo作为上下文的起始,那他们有什么区别呢?

两者的区别

这两个函数其实只是互为别名,没有差别,官方给的定义是:

  • context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。
  • context.TODO 应该只在不确定应该使用哪种上下文时使用;

所以在大多数情况下,我们都使用context.Background作为起始的上下文向下传递。

看一下两者的底层是什么?

两者其实都是一个对context的一个继承

type backgroundCtx struct{ emptyCtx }
type todoCtx struct{ emptyCtx }func Background() Context {return backgroundCtx{}
}func TODO() Context {return todoCtx{}
}

看到这里,你会发现上面的两种方式是创建根context,不具备任何功能,具体实践其实还是要依靠context包提供的With系列函数来进行派生:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

这四个函数都要基于父Context衍生,通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,画个图表示一下:

基于一个父Context可以随意衍生,其实这就是一个Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,每个子节点都依赖于其父节点,例如上图,我们可以基于Context.Background衍生出四个子contextctx1.0-cancelctx2.0-deadlinectx3.0-timeoutctx4.0-withvalue,这四个子context还可以作为父context继续向下衍生,即使其中ctx1.0-cancel 节点取消了,也不影响其他三个父节点分支。

创建context方法和context的衍生方法就这些,关于这些with函数,会在后续的使用中看到

3.1.2 With类函数

  1. WithCancel(parent Context)

用途:创建一个新的上下文和取消函数。当调用取消函数时,所有派生自这个上下文的操作将被通知取消。

应用场景:当一个长时间运行的操作需要能够被取消时。例如,用户在网页中点击“取消”按钮时,相关的数据库或 HTTP 请求应立即停止。

2. WithDeadline(parent Context, d time.Time)

用途:创建一个新的上下文,该上下文在指定的时间点自动取消。

应用场景:在请求处理时设置最大执行时间。例如,调用外部 API 时,如果响应时间超过预期,将自动取消请求,以避免无效的等待。

3. WithTimeout(parent Context, timeout time.Duration)

用途:创建一个新的上下文,它会在指定的持续时间内自动取消。

应用场景:适用于设置操作的超时时间,确保系统不会在某个操作上无休止地等待。常用于网络请求或长时间运行的任务。

4. WithValue(parent Context, key, val interface{})

用途:创建一个新的上下文,并将键值对存储在该上下文中。

应用场景:在处理请求时,将特定的数据(如用户身份信息、RequestID)在处理链中传递,而不需要在每个函数参数中显式传递。

3.2 cancelCtx

接下来看一下第二个实现的类吧,看名字就能看出来他是一个带有取消功能的上下文。

type cancelCtx struct {Contextmu       sync.Mutex            done     atomic.Value          children map[canceler]struct{} // 这里是一个set{}err      error                 cause    error                
}type canceler interface {cancel(removeFromParent bool, err, cause error)Done() <-chan struct{}
}// 这里体现了goland的一个编程哲学
// 作为一个父context,它只需要关注子类的这两个方法即可,
// 它的子类可能更有能力,但是与父亲无关,只需要知道他是否还存在即可
// 会通过就近生成interface的方式,把无关的信息都屏蔽掉。
// 也就是谁使用谁声明谁管理

• 继承了一个 context 作为其父 context. 可见,cancelCtx 必然为某个 context 的子 context

• 内置了一把锁,用以协调并发场景下的资源获取;

• done:实际类型为 chan struct{},即用以反映 cancelCtx 生命周期的通道;

• children:一个 set,指向 cancelCtx 的所有子 context;

• err:记录了当前 cancelCtx 的错误. 必然为某个 context 的子 context;

• cause:是在go1.20之后加入的字段,主要作用适用于记录导致context被取消的具体原因

在这里,要加入一些其他的内容----同步调用和异步调用

同步调用,其实就是一种串行的方式,也就是我们平时写的程序,他是一步一步进行下去,类似一条链的形式。

而异步调用则是开辟协程,并且不会阻塞主线程,并且主线程对子协程的感知能力很弱,开辟了多个子协程,就会形成类似树的形式

就会导致,主线程对子协程的管理能力下降,从而致使协程无法回收,最后导致协程泄露的一个问题。

在创建协程方面,我们要知道一点,如果不知道你创建的协程什么时候结束,你就不应该去创建,不应该滥用并发。

如何解决这个协程的控制呢?

那么今天的主角就是cancelCtx了

先说cancelCtx继承了Context,对其方法进行了一个重写,但是并没有对Deadline方法重写,而是直接继承的父类的。Deadline不进行重写是因为他没有过期取消的能力。

func (c *cancelCtx) Value(key any) any {// 这里为什么要加入这个判断,在后续会介绍,主要就是用于判断// 其自身是否是一个cancelCtx类型,这个cancelCtxKey是一个定值if key == &cancelCtxKey {return c}return value(c.Context, key)
}func (c *cancelCtx) Done() <-chan struct{} {d := c.done.Load()if d != nil {return d.(chan struct{})}c.mu.Lock()defer c.mu.Unlock()d = c.done.Load()if d == nil {d = make(chan struct{})c.done.Store(d)}return d.(chan struct{})
}func (c *cancelCtx) Err() error {c.mu.Lock()err := c.errc.mu.Unlock()return err
}

3.2.1 WithCancel取消控制

既然想实现这种父子联动的行为,就轮到了With下的一个函数WithCancel函数,通过这个函数从而得到一个cancelCtx对象,从而实现一个对子协程的一个控制

func main()  {ctx,cancel := context.WithCancel(context.Background())// 以context.Background()为父,创建得到一个子context和cancelgo Speak(ctx)time.Sleep(10*time.Second)cancel()time.Sleep(1*time.Second)
}func Speak(ctx context.Context)  {for range time.Tick(time.Second){select {case <- ctx.Done():fmt.Println("我要闭嘴了")returndefault:fmt.Println("balabalabalabala")}}
}

来对这个例子做出一个解释:

select就相当于是一个多路复用,进行一个监听的操作,通过这个WithCancel获取上下文和取消函数

当调用这个cancel函数的时候,就会直接通过这个ctx.Done发送这个取消机制,从而实现一个控制的效果

这里的操作也就是我们常说的超时控制了,当然cancel并没有涉及到超时,他是通过调用cancel()才可以实现一个关闭。

这个结构体就是cancel取消函数的结构体,他返回的是一个函数类型,所以调用的时候需要加上()

来看下这个函数的具体流程这里先提前告知一点,如果停止一个cancelCtx,则这个它下面所有的子上下文都将被杀死,具体的操作看propagateCancel函数

type CancelFunc func()func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {c := withCancel(parent)//Canceled是一个error,自定义的错误信息return c, func() { c.cancel(true, Canceled, nil) }
}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// 这一步就是说父亲是emptyCtx,他就没有必要取消// 所以没有必要大费周折的取消它,直接就返回就行done := parent.Done()if done == nil {return // parent is never canceled}// 如果父亲被取消了,那儿子也应该直接取消,记录一下取消的错误和原因select {case <-done:// parent is already canceledchild.cancel(false, parent.Err(), Cause(parent))returndefault:}// 如果我的父也是cancelCtx,则我需要将孩子加入map里面if p, ok := parentCancelCtx(parent); ok {// parent is a *cancelCtx, or derives from one.p.mu.Lock()if p.err != nil {// parent has already been canceledchild.cancel(false, p.err, p.cause)} else {if p.children == nil {p.children = make(map[canceler]struct{})}p.children[child] = struct{}{}}p.mu.Unlock()return}//检查父Context是否支持AfterFunc,也就是一个回调机制// if a, ok := parent.(afterFuncer); ok {// parent implements an AfterFunc method.c.mu.Lock()stop := a.AfterFunc(func() {child.cancel(false, parent.Err(), Cause(parent))})c.Context = stopCtx{Context: parent,stop:    stop,}c.mu.Unlock()return}// 开启一个守护协程,时刻监听,第一个判断父亲是不是被终止了// 如果父亲被终止了,就应该给孩子也砍一刀,让他们也都终止// 如果孩子被终止了,那就被终止了,什么也不需要处理,传播具有单向性goroutines.Add(1)go func() {select {case <-parent.Done():child.cancel(false, parent.Err(), Cause(parent))case <-child.Done():}}()
}

parentCancelCtx用于判断是不是cancelCtx类型

func (c *cancelCtx) Value(key any) any {// 这里为什么要加入这个判断,在后续会介绍,主要就是用于判断// 其自身是否是一个cancelCtx类型,这个cancelCtxKey是一个定值if key == &cancelCtxKey {return c}return value(c.Context, key)
}func parentCancelCtx(parent Context) (*cancelCtx, bool) {done := parent.Done()if done == closedchan || done == nil {return nil, false}p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)if !ok {return nil, false}pdone, _ := p.done.Load().(chan struct{})if pdone != done {return nil, false}return p, true
}

再看一下返回的闭包函数吧

cancelCtx.cancel 方法有三个入参,第一个 removeFromParent 是一个 bool 值,表示当前 context 是否需要从父 context 的 children set 中删除;第二个 err 则是 cancel 后需要展示的错误,第三个则表示导致错误原因。

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {if err == nil {panic("context: internal error: missing cancel error")}if cause == nil {cause = err}c.mu.Lock()if c.err != nil {c.mu.Unlock()return // already canceled}c.err = errc.cause = caused, _ := c.done.Load().(chan struct{})if d == nil {// closedchan 这里是一个全局chan// 关闭,从而取消监听,Store就是为了确保原子性和可见性c.done.Store(closedchan)} else {close(d)}for child := range c.children {// NOTE: acquiring the child's lock while holding parent's lock.child.cancel(false, err, cause)}c.children = nilc.mu.Unlock()if removeFromParent {removeChild(c.Context, c)}
}// removeChild removes a context from its parent.
func removeChild(parent Context, child canceler) {if s, ok := parent.(stopCtx); ok {s.stop()return}p, ok := parentCancelCtx(parent)if !ok {return}p.mu.Lock()if p.children != nil {delete(p.children, child)}p.mu.Unlock()
}

3.3 timerCtx

接下来看第三个实现的类

type timerCtx struct {cancelCtxtimer *time.Timer // Under cancelCtx.mu.deadline time.Time
}

timerCtx 在 cancelCtx 基础上又做了一层封装,除了继承 cancelCtx 的能力之外,新增了一个 time.Timer 用于定时终止 context;另外新增了一个 deadline 字段用于字段 timerCtx 的过期时间.

这样就有了实现时停的操作,它对Dealine进行了一个重写,其他都是继承的cancelCtx的

Deadline返回的是 deadline time.Time

3.3.1WithTimeout和WithDeadline

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {return WithDeadline(parent, time.Now().Add(timeout))
}func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {return WithDeadlineCause(parent, d, nil)
}

这两个函数他们的参数就可以看出区别,context.WithTimeout是指经过的时间段,而WithDeadline则是指时间点。

这里属于是超时取消。

3.4 valueCtx

type valueCtx struct {Contextkey, val any
}

说一下这个valueCtx吧,在不同位置设置的value其实在查找方面是有问题的,比如你在最下面那一层去存放,也就是B下面的子节点存放value,只有A和B才可以访问这个value,C和D是无法访问到的。

3.4.1WithValue

func WithValue(parent Context, key, val any) Context {if parent == nil {panic("cannot create context from nil parent")}if key == nil {panic("nil key")}if !reflectlite.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}

通过这个函数来设置value值。


http://www.hkcw.cn/article/QkbBLCVLsW.shtml

相关文章

2025年全国青少年信息素养大赛复赛C++算法创意实践挑战赛真题模拟强化训练(试卷3:共计6题带解析)

2025年全国青少年信息素养大赛复赛C++算法创意实践挑战赛真题模拟强化训练(试卷3:共计6题带解析) 第1题:四位数密码 【题目描述】 情报员使用4位数字来传递信息,同时为了防止信息泄露,需要将数字进行加密。数据加密的规则是: 每个数字都进行如下处理:该数字加上5之后除…

NeRF PyTorch 源码解读 - 体渲染

文章目录 1. 体渲染公式推导1.1. T ( t ) T(t) T(t) 的推导1.2. C ( r ) C(r) C(r) 的推导 2. 体渲染公式离散化3. 代码解读 1. 体渲染公式推导 如下图所示&#xff0c;渲染图像上点 P P P 的颜色值 c c c 是累加射线 O P → \overrightarrow{OP} OP 在近平面和远平面范围…

Sentiment analysis integrating LangGraph and large-scale conceptual models

Sentiment analysis integrating LangGraph and large-scale conceptual models 核心目标&#xff1a; 让电脑更聪明地理解大量用户评论&#xff08;比如邮件、社交媒体、调查问卷&#xff09;&#xff0c;自动分析出大家是夸还是骂&#xff08;情感分析&#xff09;&#xff…

DeepSeek R1-0528:深度思考能力的重大跃升与技术突破全解析

引言 2025年5月28日&#xff0c;DeepSeek再次以其标志性的"深夜发布"方式&#xff0c;悄然推出了R1模型的最新版本——DeepSeek-R1-0528。这次被官方定义为"小版本升级"的更新&#xff0c;实际上带来了令人瞩目的性能提升。新版本不仅在数学、编程与通用逻…

Python 训练营打卡 Day 40

训练和测试的规范写法 一、黑白图片的规范写法&#xff0c;以MNIST数据集为例 import torch import torch.nn as nn import torch.optim as optim from torchvision import datasets, transforms # 用于加载MNIST数据集 from torch.utils.data import DataLoader # 用于创建…

题海拾贝:P8598 [蓝桥杯 2013 省 AB] 错误票据

Hello大家好&#xff01;很高兴我们又见面啦&#xff01;给生活添点passion&#xff0c;开始今天的编程之路&#xff01; 我的博客&#xff1a;<但凡. 我的专栏&#xff1a;《编程之路》、《数据结构与算法之美》、《题海拾贝》 欢迎点赞&#xff0c;关注&#xff01; 1、题…

AI炼丹日志-26 - crawl4ai 专为 AI 打造的爬虫爬取库 上手指南

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; Java篇&#xff1a; MyBatis 更新完毕目前开始更新 Spring&#xff0c;一起深入浅出&#xff01; 大数据篇 300&#xff1a; Hadoop&…

homework 2025.03.31 chinese(class 3)

homework 2025.03.31 chinese&#xff08;class 3&#xff09; 三年级语文&#xff0c;古代十二时辰 ➠1. 子时&#xff08;23-1时&#xff09; “月落乌啼霜满天&#xff0c;江枫渔火对愁眠。姑苏城外寒山寺&#xff0c;夜半钟声到客船。” — 张继《枫桥夜泊》 子时夜深人静&…

若依框架定制化服务搭建

1.背景 若依框架是1套微服务框架&#xff0c;该服务在应用过程中少不了新增微服务来应对业务的需求&#xff0c;本次文档主要是针对若依框架的定制化微服务的搭建进行步骤的拆解。 2.ruoyi-api模块新建模块【report】 2.1 右键【ruoyi-api】&#xff0c;New一个Module 2.2 新…

【HW系列】—溯源与定位—Linux入侵排查

文章目录 一、Linux入侵排查1.账户安全2.特权用户排查&#xff08;UID0&#xff09;3.查看历史命令4.异常端口与进程端口排查进程排查 二、溯源分析1. 威胁情报&#xff08;Threat Intelligence&#xff09;2. IP定位&#xff08;IP Geolocation&#xff09;3. 端口扫描&#x…

JS入门——变量的类型、特殊符号、类型转化规则

JS入门——变量的类型、特殊符号、类型转化规则 一、变量类型 1.1总述 1.2代码实现 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title></head><body><script>// tyoeo可以检测出类型aler…

手写HashMap

项目仓库&#xff1a;https://gitee.com/bossDuy/hand-tear-collection-series 基于一个b站up的课程&#xff1a;https://www.bilibili.com/video/BV1SWZrYDEag?spm_id_from333.788.videopod.sections&vd_source4cda4baec795c32b16ddd661bb9ce865 手写简单的HashMap 这里…

MySQL强化关键_018_MySQL 优化手段及性能分析工具

目 录 一、优化手段 二、SQL 性能分析工具 1.查看数据库整体情况 &#xff08;1&#xff09;语法格式 &#xff08;2&#xff09;说明 2.慢查询日志 &#xff08;1&#xff09;说明 &#xff08;2&#xff09;开启慢查询日志功能 &#xff08;3&#xff09;实例 3.s…

VMware-workstation安装教程--超详细(附带安装包)附带安装CentOS系统教程

VMware-workstation安装教程--超详细&#xff08;附带安装包&#xff09;附带安装CentOS系统教程 一、下载软件VMwware二、下载需要的镜像三、在VMware上安装系统 一、下载软件VMwware 二、下载需要的镜像 三、在VMware上安装系统 VMware 被 Broadcom&#xff08;博通&#x…

Flutter - 原生交互 - 相机Camera - 01

环境 Flutter 3.29 macOS Sequoia 15.4.1 Xcode 16.3 集成 Flutter提供了camera插件来拍照和录视频&#xff0c;它提供了一系列可用的相机&#xff0c;并使用特定的相机展示相机预览、拍照、录视频。 添加依赖 camera: 提供使用设备相机模块的工具path_provider: 寻找存储图…

HackMyVM-Art

信息搜集 主机发现 ┌──(kali㉿kali)-[~] └─$ nmap -sn 192.168.43.0/24 Starting Nmap 7.95 ( https://nmap.org ) at 2025-05-31 03:00 EDT Nmap scan report for 192.168.43.1 Host is up (0.0047s latency). MAC Address: C6:45:66:05:91:88 (Unknown) Nmap scan rep…

第304个Vulnhub靶场演练攻略:digital world.local:FALL

digital world.local&#xff1a;FALL Vulnhub 演练 FALL (digitalworld.local: FALL) 是 Donavan 为 Vulnhub 打造的一款中型机器。这款实验室非常适合经验丰富的 CTF 玩家&#xff0c;他们希望在这类环境中检验自己的技能。那么&#xff0c;让我们开始吧&#xff0c;看看如何…

使用 HTML + JavaScript 在高德地图上实现物流轨迹跟踪系统

在电商行业蓬勃发展的今天&#xff0c;物流信息查询已成为人们日常生活中的重要需求。本文将详细介绍如何基于高德地图 API 利用 HTML JavaScript 实现物流轨迹跟踪系统的开发。 效果演示 项目概述 本项目主要包含以下核心功能&#xff1a; 地图初始化与展示运单号查询功能…

HTML Day04

Day04 0.引言1. HTML字符实体2. HTML表单2.1 表单标签2.2 表单示例 3. HTML框架4. HTML颜色4.1 16进制表示法4.2 rgba表示法4.3 名称表达法 5. HTML脚本 0.引言 刚刚回顾了前面几篇博客&#xff0c;感觉写的内容倒是很详细&#xff0c;每个知识点都做了说明。但是感觉在知识组织…

命令行式本地与服务器互传文件

文章目录 1. 背景2. 传输方式2.1 SCP 协议传输2.2 SFTP 协议传输 命令行式本地与服务器互传文件 1. 背景 多设备协同工作中&#xff0c;因操作系统的不同&#xff0c;我们经常需要将另外一个系统中的文件传输到本地PC进行浏览、编译。多设备文件互传&#xff0c;在嵌入式开发中…