golang-context详解

news2025/4/18 8:32:41

Context是什么

cancel 其实就是通过chan + select进行提前中断返回
如果没有context,携程之间怎么做这些交互呢?肯定也能做
跨线程通讯如共享内存,pipe等等都可以做到,但是就需要开发者对通讯设计建模、规划数据同步方式等,这里面还需要考虑内存管理和死锁问题。context相当于程序已经提供了这样一套设计方案(CSP),可以很方便使用。也符合golang的“简单”哲学。
在跨线程场景下,相当于提供了官方的解决方案

Context 只有两个简单的功能:跨 API 或在进程间 1)携带键值对、2)传递取消信号(主动取消、时限/超时自动取消)

Context是Go 语言在 1.7 版本中引入的一个标准库的接口,其定义如下:

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

这个接口定义了四个方法:

  • Deadline: 设置 context.Context 被取消的时间,即截止时间;
  • Done: 返回一个 只读Channel,当Context被取消或者到达截止时间,这个 Channel 就会被关闭,表示Context的链路结束,多次调用 Done 方法会返回同一个 Channel
  • Err: 返回 context.Context 结束的原因,它只会在 Done 返回的 Channel 被关闭时才会返回非空的值,返回值有以下两种情况;
    • 如果是context.Context 被取消,返回 Canceled
    • 如果是context.Context 超时,返回 DeadlineExceeded
  • Value — 从 context.Context 中获取键对应的值,类似于Map的get方法,对于同一个context,多次调用 Value 并传入相同的 Key会返回相同的结果,如果没有对应的Key,则返回nil,键值对是通过WithValue方法写入的

Context创建

根Context创建

主要有以下两种方式创建根context

context.Backgroud()
context.TODO()

从源代码分析context.Backgroundcontext.TODO 并没有太多的区别,都是用于创建根context,根context是一个空的context,不具备任何功能。但是一般情况下,如果当前函数没有上下文作为入参,我们都会使用 context.Background创建一个根context 作为起始的上下文向下传递

这两个函数的实现都返回一个 context.emptyCtx 对象的地址:

var (
 background = new(emptyCtx)
 todo       = new(emptyCtx)
)

尽管本质上是一样的,但是区分两个函数是为了在编写代码时,更清晰地表明开发人员在创建这个上下文的意图:

  • TODO(): 不确定要使用哪个上下文时,可以将其用作占位符
  • BackGround(): 打算启动已知上下文的地方,通常我们都使用这个

空的上下文没有什么用途。因为 emptyCtx 的 Done()、Err()、Value() 等方法,都返回的 nil。

子Context创建

根context在创建之后,不具备任何的功能,为了让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,每个with函数都会创建出一个新的context,这样类似于我们熟悉的树结构,当前context称为父context,派生出的新context称为子context。就像下面的context树结构:
在这里插入图片描述

Context有什么用

Context主要有两个用途,也是在项目中经常使用的

  • 用于并发控制,控制协程的优雅退出
  • 上下文的信息传递
    总的来说,Context就是用来在父子goroutine间进行值传递以及发送cancel信号的一种机

并发控制

对于一般的服务器而言,都是一致运行着的,等待接收来自客户端或者浏览器的请求做出响应,思考这样一种场景,后台微服务架构中,一般服务器在收到一个请求之后,如果逻辑复杂,不会在一个goroutine中完成,而是会创建出很多的goroutine共同完成这个请求,就像下面这种情况
在这里插入图片描述

有一个请求过来之后,先经过第一次rpc1调用,然后再到rpc2,后面创建执行两个rpc,rpc4里又有一次rpc调用rpc5,等所有 rpc 调用成功后,返回结果。假如在整个调用过程中,rpc1发生了错误,如果没有context存在的话,我们还是得等所有的rpc都执行完才能返回结果,这样其实浪费了不少时间,因为一旦出错,我们完全可以直接再rpc1这里就返回结果了,不用等到后续的rpc都执行完。假设我们在rpc1直接返回失败,不等后续的rpc继续执行,那么其实后续的rpc执行就是没有意义的,浪费计算和IO资源而已。再引入context之后,就可以很好的处理这个问题,在不需要子goroutine执行的时候,可以通过context通知子goroutine优雅的关闭

Done()—— 确定上下文是否完成

无论上下文是因为什么原因结束的,都可以通过调用其 Done() 方法确认:该方法返回一个通道(chan struct{}),该通道会在上下文完成时被关闭,任何监听该通道的函数都会感应到对应上下文完成的事件。

【channel 基础知识】通道有一种常见的用法:不会往通道里写入任何东西,在需要发送信号的时候关闭通道,此时接收操作符(receive operator)会立马收到一个管道类型的零值

通道的等待往往结合 select 一块使用。 select 可以通过多个 case 同时读取多个 channel,如果每个 case 的 channel 都被阻塞则 select 会被阻塞。也会有另外的做法,在 default 做逻辑或者 sleep,将 select 放在循环中,不断的重复检查。

再说回 Done() 方法,它返回一个通道,在 Context 未关闭和关闭的表现:

  • 没有关闭的时候,case <- ctx.Done() 会阻塞住
  • 关闭之后,每次 <- ctx.Done() 都会返回一个零值

下面context.WithCancel一节会有使用示例

context.WithCancel

方法定义如下:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

context.WithCancel 函数是一个取消控制函数,只需要一个context作为参数,能够从 context.Context 中衍生出一个新的子context和取消函数CancelFunc,通过将这个子context传递到新的goroutine中来控制这些goroutine的关闭,一旦我们执行返回的取消函数CancelFunc,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到取消信号

使用示例:

package main

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

func main() {
   ctx, cancel := context.WithCancel(context.Background())
   go Watch(ctx, "goroutine1")
   go Watch(ctx, "goroutine2")

   time.Sleep(6 * time.Second)   // 让goroutine1和goroutine2执行6s
   fmt.Println("end watching!!!")
   cancel()  // 通知goroutine1和goroutine2关闭
   time.Sleep(1 * time.Second)
}

func Watch(ctx context.Context, name string) {
   for {
      select {
      case <-ctx.Done():
         fmt.Printf("%s exit!\n", name) // 主goroutine调用cancel后,会发送一个信号到ctx.Done()这个channel,这里就会收到信息
         return
      default:
         fmt.Printf("%s watching...\n", name)
         time.Sleep(time.Second)
      }
   }
}

运行结果:

goroutine2 watching...
goroutine1 watching...
goroutine1 watching...
goroutine2 watching...
goroutine2 watching...
goroutine1 watching...
goroutine1 watching...
goroutine2 watching...
goroutine2 watching...
goroutine1 watching...
goroutine1 watching...
goroutine2 watching...
end watching!!!
goroutine1 exit!
goroutine2 exit!

ctx, cancel := context.WithCancel(context.Background())派生出了一个带有返回函数cancel的ctx,并把它传入到子goroutine中,接下来在6s时间内,由于没有执行cancel函数,子goroutine将一直执行default语句,打印监控。6s之后,调用cancel,此时子goroutine会从ctx.Done()这个channel中收到消息,执行return结束

context.WithDeadline

方法定义如下:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

context.WithDeadline也是一个取消控制函数,方法有两个参数,第一个参数是一个context,第二个参数是截止时间,同样会返回一个子context和一个取消函数CancelFunc。在使用的时候,没有到截止时间,我们可以通过手动调用CancelFunc来取消子context,控制子goroutine的退出,如果到了截止时间,我们都没有调用CancelFunc,子context的Done()管道也会收到一个取消信号,用来控制子goroutine退出

package main

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

func main() {
   ctx, cancel := context.WithDeadline(context.Background(),time.Now().Add(4*time.Second)) // 设置超时时间4当前时间4s之后
   defer cancel()
   go Watch(ctx, "goroutine1")
   go Watch(ctx, "goroutine2")

   time.Sleep(6 * time.Second)   // 让goroutine1和goroutine2执行6s
   fmt.Println("end watching!!!")
}

func Watch(ctx context.Context, name string) {
   for {
      select {
      case <-ctx.Done():
         fmt.Printf("%s exit!\n", name) // 4s之后收到信号
         return
      default:
         fmt.Printf("%s watching...\n", name)
         time.Sleep(time.Second)
      }
   }
}

运行结果:

goroutine1 watching...
goroutine2 watching...
goroutine2 watching...
goroutine1 watching...
goroutine1 watching...
goroutine2 watching...
goroutine1 exit!
goroutine2 exit!
end watching!!!

我们并没有调用cancel函数,但是在过了4s之后,子groutine里ctx.Done()收到了信号,打印出exit,子goroutine退出,这就是WithDeadline派生子context的用法

context.WithTimeout

方法定义:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

context.WithTimeout和context.WithDeadline的作用类似,都是用于超时取消子context,只是传递的第二个参数有所不同,context.WithTimeout传递的第二个参数不是具体时间,而是时间长度

package main

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

func main() {
   ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
   defer cancel()
   go Watch(ctx, "goroutine1")
   go Watch(ctx, "goroutine2")

   time.Sleep(6 * time.Second)   // 让goroutine1和goroutine2执行6s
   fmt.Println("end watching!!!")
}

func Watch(ctx context.Context, name string) {
   for {
      select {
      case <-ctx.Done():
         fmt.Printf("%s exit!\n", name) // 主goroutine调用cancel后,会发送一个信号到ctx.Done()这个channel,这里就会收到信息
         return
      default:
         fmt.Printf("%s watching...\n", name)
         time.Sleep(time.Second)
      }
   }
}

运行结果:

goroutine2 watching...
goroutine1 watching...
goroutine1 watching...
goroutine2 watching...
goroutine2 watching...
goroutine1 watching...
goroutine1 watching...
goroutine2 watching...
goroutine1 exit!
goroutine2 exit!
end watching!!!

程序很简单,与上个context.WithDeadline的样例代码基本一样,只是改变了下派生context的方法为context.WithTimeout,具体体现在第二个参数不再是具体时间,而是变为了4s这个具体的时间长度,执行结果也是一样

上下文的信息传递

context.WithValue

方法定义:

func WithValue(parent Context, key, val interface{}) Context

context.WithValu 函数从父context中创建一个子context用于传值,函数参数是父context,key,val键值对。返回一个context 项目中这个方法一般用于上下文信息的传递,比如请求唯一id,以及trace_id等,用于链路追踪以及配置透传

使用示例:

package main

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

func func1(ctx context.Context) {
   fmt.Printf("name is: %s", ctx.Value("name").(string))
}

func main() {
   ctx := context.WithValue(context.Background(), "name", "zhangsan")
   go func1(ctx)
   time.Sleep(time.Second)
}

运行结果:

name is: zhangsan

WithValue() 函数定义中,传入的 Context 命名为 parent:再次强调,返回的 Context 与其是派生关系。

func main() {
 a := context.Background()              // 创建上下文
 b := context.WithValue(a, "k1", "v1")  // 塞入一个kv
 c := context.WithValue(b, "k2", "v2")  // 塞入另外一个kv
 d := context.WithValue(c, "k1", "vo1") // 覆盖一个kv

 fmt.Printf("k1 of b: %s\n", b.Value("k1"))
 fmt.Printf("k1 of d: %s\n", d.Value("k1"))
 fmt.Printf("k2 of d: %s\n", d.Value("k2"))
}

上述代码打印的内容:

k1 of b: v1
k1 of d: vo1
k2 of d: v2
直观的感觉是上下文中的键值对可以被覆盖,但看一下 WithValue() 的实现,这种表现并不是真正的覆盖了某些值。

另外,这里的值可以是任何类型,拿出来使用的时候,需要转换成具体的类型。

实现原理
每次调用 WithValue() 函数,会返回一个 *valueCtx 的指针,将 key, val 设置进去(val 可以是任何类型),并且将之前的 Context 嵌套进去。

valueCtx 的结构如下:

type valueCtx struct {
 Context
 key, val any
}

而 Context.Value() 方法,则是不断的从 Context 中寻找是否有对应的 key 匹配,如果匹配则返回 val;如果不匹配在包裹的 Context 中继续寻找。

如果不断地通过 WithValue() 同一个的 key 更新上下文,写入和读取就像使用一个栈,后边被设置进去的会被先读取到。 Value() 是一个递归解嵌套的过程,终止条件就是 Context 为 emptyCtx 或找到对应 key。

Context 的不足

Context 的作用很明显,当我们在开发后台服务时,能帮助我们完成对一组相关 goroutine 的控制并传递共享数据。注意是后台服务,而不是所有的场景都需要使用 Context。

Go 官方建议我们把 Context 作为函数的第一个参数,甚至连名字都准备好了。这造成一个后果:因为我们想控制所有的协程的取消动作,所以需要在几乎所有的函数里加上一个 Context 参数。很快,我们的代码里,context 将像病毒一样扩散的到处都是。

另外,像 WithCancel、WithDeadline、WithTimeout、WithValue 这些创建函数,实际上是创建了一个个的链表结点而已。我们知道,对链表的操作,通常都是 O(n) 复杂度的,效率不高。

Context 解决的核心问题是 cancelation,即便它不完美,但它却简洁地解决了这个问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2335229.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Boot 集成 RocketMQ 全流程指南:从依赖引入到消息收发

前言 在分布式系统中&#xff0c;消息中间件是解耦服务、实现异步通信的核心组件。RocketMQ 作为阿里巴巴开源的高性能分布式消息中间件&#xff0c;凭借其高吞吐、低延迟、高可靠等特性&#xff0c;成为企业级应用的首选。而 Spring Boot 通过其“约定优于配置”的设计理念&a…

AI与我共创WEB界面

记录一次压测后的自我技术提升 这事儿得从机房停电说起。那天吭哧吭哧做完并发压测,正准备截Zabbix监控图写报告,突然发现监控曲线神秘失踪——系统组小哥挠着头说:“上次停电后,zabbix服务好像就没起来过…” 我盯着空荡荡的图表界面,大脑的CPU温度可能比服务器还高。 其…

基于频率约束条件的最小惯量需求评估,包括频率变化率ROCOF约束和频率最低点约束matlab/simulink

基于频率约束条件的最小惯量评估&#xff0c;包括频率变化率ROCOF约束和频率最低点约束matlab/simulink 1建立了含新能源调频的频域仿真传函模型&#xff0c;虚拟惯量下垂控制 2基于构建的模型&#xff0c;考虑了不同调频系数&#xff0c;不同扰动情况下的系统最小惯量需求

深入理解浏览器的 Cookie:全面解析与实践指南

在现代 Web 开发中&#xff0c;Cookie 扮演着举足轻重的角色。它不仅用于管理用户会话、记录用户偏好&#xff0c;还在行为追踪、广告投放以及安全防护等诸多方面发挥着重要作用。随着互联网应用场景的不断丰富&#xff0c;Cookie 的使用和管理也日趋复杂&#xff0c;如何在保障…

Java 正则表达式综合实战:URL 匹配与源码解析

在 Web 应用开发中&#xff0c;我们经常需要对 URL 进行格式验证。今天我们结合 Java 的 Pattern 和 Matcher 类&#xff0c;深入理解正则表达式在实际应用中的强大功能&#xff0c;并剖析一段实际的 Java 示例源码。 package com.RegExpInfo;import java.util.regex.Matcher; …

【C++】前向声明(Forward Declaration)

前向声明&#xff08;Forward Declaration&#xff09;是在C、C等编程语言中&#xff0c;在使用一个类、结构体或其他类型之前&#xff0c;仅声明其名称而不给出完整定义的一种方式。 作用 减少编译依赖&#xff1a;当一个源文件包含大量头文件时&#xff0c;编译时间会显著增…

numpy.ma.masked_where:屏蔽满足条件的数组

1.函数功能 屏蔽满足条件的数组内容&#xff0c;返回值为掩码数组 2.语法结构 np.ma.masked_where(condition, a, copyTrue)3. 参数 参数含义condition屏蔽条件a要操作的数组copy布尔值&#xff0c;取值为True时&#xff0c;结果复制数组(原始数据不变)&#xff0c;否则返回…

【解决】bartender软件换网之后神秘变慢

下的山寨版本bartender软件&#xff0c;用着一直都挺好&#xff0c;结果一次换网之后&#xff0c;启动&#xff0c;排版&#xff0c;打印各种动作都要转个几分钟才行&#xff0c;非常奇怪。直接说解决过程。 首先联想网络没有动以及脱机的时候&#xff0c;都没有这个问题。那么…

[福游宝——AI智能旅游信息查询平台]全栈AI项目-阶段二:聊天咨询业务组件开发

简言 本项目旨在构建一个以AI智能体为核心的福建省旅游信息查询系统&#xff0c;聚焦景点推荐、路线规划、交通天气查询等功能&#xff0c;为游客提供智能化、便捷化的旅游信息服务。项目采用前后端分离架构&#xff0c;前端基于Vite TypeScript Vue3技术栈&#xff0c;搭配…

【教学类-102-11】蝴蝶外轮廓01——Python对黑白图片进行PS填充三种颜色+图案描边+图案填充白色+制作1图2图6图24图

背景需求: 用Python,对白色255背景的图片进行了透明化、制作点状或线段的描边裁剪线 【教学类-102-10】剪纸图案全套代码09——Python线条虚线优化版04(原图放大白背景)+制作1图2图6图24图-CSDN博客文章浏览阅读1k次,点赞27次,收藏8次。【教学类-102-10】剪纸图案全套代…

MCP的另一面

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

微信小程序 - swiper轮播图

官方文档&#xff1a;https://developers.weixin.qq.com/miniprogram/dev/component/swiper.html <swiper indicator-color"ivory" indicator-active-color"#d43c33" indicator-dots autoplay><swiper-item><image src"/images/banner…

2025年第十六届蓝桥杯省赛C++ 研究生组真题

2025年第十六届蓝桥杯省赛C 研究生组真题 1.说明2.题目A&#xff1a;数位倍数&#xff08;5分&#xff09;3.题目B&#xff1a;IPv6&#xff08;5分&#xff09;4.题目C&#xff1a;变换数组&#xff08;10分&#xff09;5.题目D&#xff1a;最大数字&#xff08;10分&#xff…

七、自动化概念篇

自动化测试概念 自动化测试是把以人为驱动的测试行为转化为机器执行的一种过程。通常&#xff0c;在设计了测试用例并通过评审之后&#xff0c;由测试人员根据测试用例中描述的过程一步步执行测试&#xff0c;得到实际结果与期望结果的比较。在此过程中&#xff0c;为了节省人…

【第43节】实验分析windows异常分发原理

目录 前言 一、异常处理大致流程图 二、实验一&#xff1a;分析 KiTrap03 三、实验二&#xff1a;分析CommonDispatchException 四、代码探究&#xff1a;分析 KiDispatchException 函数 五、代码探究&#xff1a;伪代码分析用户层KiUserExceptionDispatcher 前言 在Wind…

如何在AMD MI300X 服务器上部署 DeepSeek R1模型?

DeepSeek-R1凭借其深度推理能力备受关注&#xff0c;在语言模型性能基准测试中可与顶级闭源模型匹敌。 AMD Instinct MI300X GPU可在单节点上高效运行新发布的DeepSeek-R1和V3模型。 用户通过SGLang优化&#xff0c;将MI300X的性能提升至初始版本的4倍&#xff0c;且更多优化将…

RTX 5060 Ti 3DMark跑分首次流出:比RTX 4060 Ti快20%

快科技4月14日消息&#xff0c;根据VideoCardz拿到的数据&#xff0c;RTX 5060 Ti 16GB在3DMark的系列基准测试中&#xff0c;平均较上一代RTX 4060 Ti 16GB高出20%。 具体来看&#xff0c;RTX 5060 Ti 16GB在3DMark的测试中表现如下&#xff1a; TimeSpy&#xff08;1440p&a…

【STL】set

在 C C C S T L STL STL 标准库中&#xff0c; s e t set set 是一个关联式容器&#xff0c;表示一个集合&#xff0c;用于存储唯一元素的容器。 s e t set set 中的元素会自动按照一定的顺序排序&#xff08;默认情况下是升序&#xff09;。这意味着在 s e t set set 中不能…

深入剖析C++中 String 类的模拟实现

目录 引言 一、基础框架搭建 成员变量与基本构造函数 析构函数 二、拷贝与赋值操作 深拷贝的拷贝构造函数 赋值运算符重载 三、字符串操作功能实现 获取字符串长度 字符串拼接 字符串比较 字符访问 四、迭代器相关实现&#xff08;简单模拟&#xff09; 迭代器类型…

STL之priority_queue的用法与实现

目录 1. priority_queue的介绍 1.1. priority_queue的概念 1.2. priority_queue的特点 2. 仿函数 2.1. 仿函数的概念 2.2. 仿函数的应用 2.3 仿函数的灵活性 3. priority_queue的用法 4. 模拟实现priority_queue 4.1. 插入 4.2. 删除 5. 源码 priority_…