Go 上下文的理解与使用
为什么需要 context
在 Go 程序中,特别是并发情况下,由于超时、取消等而引发的异常操作,往往需要及时的释放相应资源,正确的关闭 goroutine。防止协程不退出而导致内存泄露。如果没有 context,用来控制协程退出将会非常麻烦,我们来举一个例子。
假如说现在一个协程A开启了一个子协程B,这个子协程B又开启了另外两个子协程B1和B2来运行不同的任务,协程B2又开启了协程C来运行其他任务,现在协程A通知子协程B该退出了,这个时候我们需要完成这样的操作:A通知B退出,B退出时通知B1、B2退出,B2退出时通知C退出:
func TestChanCloseGoroutine(t *testing.T) {
fmt.Printf("开始了,有%d个协程\n", runtime.NumGoroutine())
var (
chB = make(chan struct{})
chB1 = make(chan struct{})
chB2 = make(chan struct{})
chC = make(chan struct{})
)
// 协程A
go func() {
// 协程B
go func() {
// 协程B1
go func() {
for {
select {
case <-chB1:
return
default:
}
}
}()
// 协程B2
go func() {
// 协程C
go func() {
for {
select {
case <-chC:
return
default:
}
}
}()
for {
select {
case <-chB2:
// 通知协程C退出
chC <- struct{}{}
return
default:
}
}
}()
for {
select {
case <-chB:
chB1 <- struct{}{}
chB2 <- struct{}{}
return
default:
}
}
}()
// 1秒后通知B退出
time.Sleep(1 * time.Second)
chB <- struct{}{}
// A后续没有任务了,会自动退出
}()
time.Sleep(2 * time.Second)
fmt.Printf("最终结束,有%d个协程\n", runtime.NumGoroutine())
}
// 结果
开始了,有2个协程
最终结束,有2个协程
// tips: Go Test 会启动两个额外的 goroutine 来运行代码,所以初始就会有2个 goroutine
通过 channel 来控制各个 goroutine 的关闭,程序看上去一点也不优雅。而且这才仅仅四个 goroutine ,就已经显得有些力不从心了,在真实的业务中,哪怕一个简单的 http 请求,都不可能启用四个 goroutine 就能够完成,且子协程的层级也绝非只有寥寥的三层!
context 是什么
context 在 Go 中是一个接口,它的定义如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
- Deadline 用来获取 ctx 的截止时间,如果没有截至时间,ok 将返回 false;
- Done 里面是一个通道,当 ctx 被取消时,会返回一个关闭的 channel,如果该 ctx 永远都不会被关闭,则返回 nil;
- Err 返回的 ctx 取消的原因,如果 ctx 没有被取消,会返回 nil。如果已经关闭了,会返回被关闭的原因,如果是被取消的会返回 canceled,超时的显示 deadline exceeded;
- Value 会返回 ctx 中储存的值,会从当前 ctx 中一路向上追溯,如果整条 ctx 链中都没有找到值,则会返回nil。
context 的基本结构比较简单,里面也只有四个方法,如果到此没有理解四个方法也没有关系,下文会使用到这四个方法,届时将会很自然的掌握它们。
context 接口的实现
context 有四个不同的实现:emptyCtx、cancelCtx、timerCtx、valueCtx:
type emptyCtx int
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 接口的整型,它不能储存信息,也不能被取消,它被当作根节点 ctx。cancelCtx、timerCtx、valueCtx 由于篇幅原因,这里不放出它们的源码,只解释它们的作用:cancelCtx 是一个可以主动取消的 ctx。timerCtx 也是一个可以主动取消的 ctx,不同于 cancelCtx,它还储存着额外的时间信息,当时间条件满足后,会自动取消该 ctx,利用这点,可以实现超时机制。valueCtx 比较简单,用来创建一个携带键值的 ctx。
context 的基本使用
创建一个根节点
创建根节点有两种方法:
ctx := context.Background()
ctx := context.TODO()
这两种方法其实本质上都是初始化了一个 emptyCtx:
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
可以看到,在代码中,这两个函数其实是一模一样的,只是用于不同场景下:Background 推荐在主函数、初始化和测试中使用,TODO 用于不清楚使用哪个 context 时使用。根节点 ctx 不具备任何意义,也不能被取消。
创建一个子 ctx
可以通过WithCancel、WithDeadline、WithTimeout、WithValue 这四个主要的函数来创建子 ctx ,创建一个子 ctx 必须指定其归属的父 ctx,由此来形成一个上下文链,用来同步 goroutine 信号。来看一下它们的简单使用:
WithCancel 用来创建一个 cancelCtx,它可以被主动取消 :
func TestCtxWithCancel(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
go func() {
for {
select {
// 还记得前文提到的Done的方法吗
// 当 ctx 取消时,ctx.Done()对应的通道就会关闭,case也就会被执行
case <-ctx.Done():
// ctx.Err() 会获取到关闭原因哦
fmt.Println("协程关闭", ctx.Err())
return
default:
fmt.Println("继续运行")
time.Sleep(100 * time.Millisecond)
}
}
}()
// 等待一秒后关闭
time.Sleep(1 * time.Second)
cancel()
// 等待一秒,让子协程有时间打印出协程关闭的原因
time.Sleep(1 * time.Second)
}
// 结果
继续运行
继续运行
……
协程关闭 context canceled
WithDeadline 用来创建一个 timerCtx,当时间条件满足后,它会被自动取消 :
func TestCtxWithDeadline(t *testing.T) {
ctx := context.Background()
// 等待2秒后自动关闭
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2*time.Second))
defer cancel()
// Deadline 前文也提到了,还记得吗?用来获取当前任务的截至时间
if t, ok := ctx.Deadline(); ok {
// time.DateTime 是 go1.20 版本的一个常量,其值是:"2006-01-02 15:04:05"
fmt.Println(t.Format(time.DateTime))
}
go func() {
select {
case <-ctx.Done():
// 手动关闭 context canceled
// 自动关闭 context deadline exceeded
fmt.Println("协程关闭", ctx.Err())
return
}
}()
time.Sleep(3 * time.Second)
}
// 结果
2023-05-10 18:00:36
协程关闭 context deadline exceeded
// 将最后的等待时间更改为一秒
func TestCtxWithDeadline(t *testing.T) {
……
time.Sleep(1 * time.Second)
}
// 结果
2023-05-10 18:01:45
协程关闭 context canceled
哪怕 WithDeadline 到达指定时间会自动关闭,但依然推荐使用 defer cancel() 。这是因为如果任务已经完成了,但是自动取消仍需要1天时间,那么系统就会白白浪费资源在这1天上。
WithTimeout 与 WithDeadline 同理,只不过是 WithTimeout 用来接受一个过期时间,而不是接受一个过期时间节点:
func TestCtxWithTimeout(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
go func() {
select {
case <-ctx.Done():
fmt.Println("协程关闭", ctx.Err())
return
}
}()
time.Sleep(3 * time.Second)
}
// 结果
协程关闭 context deadline exceeded
WithValue 用来创建一个 valueCtx:
// 向上找到最近的上下文值
func TestCtxWithValue(t *testing.T) {
ctx := context.Background()
ctx1 := context.WithValue(ctx, "key", "ok")
ctx2, _ := context.WithCancel(ctx1)
// Value 会一直向上追溯到根节点,获取当前上下文携带的值,
value := ctx2.Value("key")
if value != nil {
fmt.Println(value)
}
}
// 结果
ok
这四个函数都是创建一个新的子节点,并不是直接修改当前 ctx,所以最后生成的 ctx 链有可能是这样的:

使用 ctx 退出 goroutine
回到开头提到的那个例子,我们使用 context 对其改造一下:
func TestCtxCloseGoroutine(t *testing.T) {
fmt.Printf("开始了,有%d个协程\n", runtime.NumGoroutine())
ctx := context.Background()
// 协程A
go func(ctx context.Context) {
ctx, cancel := context.WithCancel(ctx)
// 协程B
go func(ctx context.Context) {
// 协程B1
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
}
}
}(ctx)
// 协程B2
go func(ctx context.Context) {
// 协程C
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
}
}
}(ctx)
for {
select {
case <-ctx.Done():
return
default:
}
}
}(ctx)
for {
select {
case <-ctx.Done():
return
default:
}
}
}(ctx)
// 1秒后通知退出
time.Sleep(1 * time.Second)
cancel()
// A后续没有任务了,会自动退出
}(ctx)
time.Sleep(2 * time.Second)
fmt.Printf("最终结束,有%d个协程\n", runtime.NumGoroutine())
}
// 结果
开始了,有2个协程
最终结束,有2个协程
可以看到,和使用 channel 控制 goroutine 退出相比,context 大大降低了心智负担。context 优雅的实现了某一层任务退出,下层所有任务退出,上层任务和同层任务不受影响。
Go 语言最佳实践:每次 context 的传递都应该直接使用值传递,不应该使用指针传递。这样可以防止上下文的值被多个并发的 goroutine 修改而导致竞争问题。虽然使用值传递会导致一些微小的性能开销,因为每次传递上下文时都需要复制一份数据,但它提供了更好的并发安全性和程序可靠性。另外,由于上下文采用了值传递,也不应该向上下文中存入较大的数据,从而导致性能问题。
Go 上下文的理解与使用的更多相关文章
- 对于Javascript 执行上下文的理解
转载无源头地址 在这篇文章中,将比较深入地阐述下执行上下文 – JavaScript中最基础也是最重要的一个概念.相信读完这篇文章后,你就会明白javascript引擎内部在执行代码以前到底做了些什么 ...
- Linux内核中进程上下文、中断上下文、原子上下文、用户上下文的理解【转】
转自:http://blog.csdn.net/laoliu_lcl/article/details/39972459 进程上下文和中断上下文是操作系统中很重要的两个概念,这两个概念在操作系统课程中不 ...
- 对Linux内核中进程上下文和中断上下文的理解
内核空间和用户空间是操作系统理论的基础之一,即内核功能模块运行在内核空间,而应用程序运行在用户空间.现代的CPU都具有不同的操作模式,代表不同的 级别,不同的级别具有不同的功能,在较低的级别中将禁止某 ...
- javascript 执行上下文的理解
首先,为什么某些函数以及变量在没有被声明以前就可以被使用,javascript引擎内部在执行代码以前到底做了些什么?这里,想信大家都会想到,变量声明提前这个概念: 但是,以下我要讲的是,声明提前的这个 ...
- 201709015工作日记--上下文的理解,ASM
1.Android上下文理解 Android上下文对象,在Context中封装一个所谓的“语境”,Activity.Service.Application都继承自Context,所以在这三者创建时都会 ...
- Linux内核中进程上下文和中断上下文的理解
參考: http://www.embedu.org/Column/Column240.htm http://www.cnblogs.com/Anker/p/3269106.html 首先明白一个概念: ...
- spring学习-ApplicationContext-spring上下文深入理解
4月份开始复习一遍spring相关知识.让自己巩固一下spring大法的深奥益处,所以就看了大佬的博客,转载留下来日后继续研读.认为重点的标记为红色 以下文章内容转载自:http://www.cnbl ...
- 理解和使用NT驱动程序的执行上下文
理解Windows NT驱动程序最重要的概念之一就是驱动程序运行时所处的“执行上下文”.理解并小心地应用这个概念可以帮助你构建更快.更高效的驱动程序. NT标准内核模式驱动程序编程的一个重要观念是某个 ...
- JS底层知识理解之执行上下文篇
JS底层知识理解之执行上下文篇 一.什么是执行上下文(Execution Context) 执行上下文可以理解为当前代码的执行环境,它会形成一个作用域. 二.JavaScript引擎会以什么方式去处理 ...
- linux 用户态和内核态以及进程上下文、中断上下文 内核空间用户空间理解
1.特权级 Intel x86架构的cpu一共有0-4四个特权级,0级最高,3级最低,ARM架构也有不同的特权级,硬件上在执行每条指令时都会对指令所具有的特权级做相应的检查.硬件已经提 ...
随机推荐
- 2023-05-18:有 n 名工人。 给定两个数组 quality 和 wage , 其中,quality[i] 表示第 i 名工人的工作质量,其最低期望工资为 wage[i] 。 现在我们想雇佣
2023-05-18:有 n 名工人. 给定两个数组 quality 和 wage , 其中,quality[i] 表示第 i 名工人的工作质量,其最低期望工资为 wage[i] . 现在我们想雇佣 ...
- [MAUI]模仿Chrome下拉标签页的交互实现
@ 目录 创建粘滞效果的圆控件 贝塞尔曲线绘制圆 创建控件 创建形变 可控形变 形变边界 形变动画 创建手势控件 创建页面布局 更新拖拽物位置 其它细节 项目地址 今天来说说怎样在.NET MAUI中 ...
- Java(if选择、switch选择、循环)
1.if 选择结构 //语法 if(表达式){ //语句:(表达式为真) }else{ //语句:(表达式为假) } --------------------------------------- 例 ...
- .net 温故知新【11】:Asp.Net Core WebAPI 入门使用及介绍
在Asp.Net Core 上面由于现在前后端分离已经是趋势,所以asp.net core MVC用的没有那么多,主要以WebApi作为学习目标. 一.创建一个WebApi项目 我使用的是VS2022 ...
- 你是怎么学习 Java 技术的?
一.Java 语言 Java 语言不只是一门语言. Java 学习不是一蹴而就就可以达成的,它是一个循序渐进,由浅入深,由表及里的过程.尤其需要注意的是不能有浅尝辄耻,不求甚解的态度.每个地方只抓一点 ...
- App性能测试之SoloPi
SoloPi简介 SoloPi是蚂蚁金服开发的一款无线化.非侵入.免Root的Android专项测试工具.直接操控安卓系统的手机或智能设备,即可完成自动化的功能.性能.兼容性.以及稳定性测试等工作,降 ...
- 驱动开发:内核ShellCode线程注入
还记得<驱动开发:内核LoadLibrary实现DLL注入>中所使用的注入技术吗,我们通过RtlCreateUserThread函数调用实现了注入DLL到应用层并执行,本章将继续探索一个简 ...
- 了解web网络基础
TCP/IP 协议:一种规则,规定不同计算机操作系统,硬件之间怎么通信的一种规则 像这样把互联网相关联的协议集合起来总称为TCP/IP协议. TCP/IP分层管理 按照组层次分为以下四层: 应用层:决 ...
- Set_HashSet_TreeSet_小记
Set接口:Set集合继承自Collection集合 Set:底层数据结构是一个哈希表,能保证元素是唯一的,元素不重复!它通过它的子实现了HashSet集合去实例化,HashSet集合底层是HashM ...
- EC600U-4G模组,连接阿里云测试服务器和物联网平台
原博主视频:https://www.bilibili.com/video/BV1yT4y1P7Gw?share_source=copy_web 连接阿里云服务器 !!需要公网ip(服务器)才能远程,不 ...