深入理解 Go 语言中的 Context
本篇文章主要是结合我之前面试的各种经历和实战开发中遇到的问题解决经验整理的,希望这篇《深入理解 Go 语言中的 Context》对你有很大帮助!欢迎收藏,分享给更多的需要的朋友学习~
Hi,大家好,我是明哥。
在自己学习 Golang 的这段时间里,我写了详细的学习笔记放在我的个人微信公众号 《Go编程时光》,对于 Go 语言,我也算是个初学者,因此写的东西应该会比较适合刚接触的同学,如果你也是刚学习 Go 语言,不防关注一下,一起学习,一起成长。
我的在线博客:http://golang.iswbm.com
我的 Github:github.com/iswbm/GolangCodingTime
1. 什么是 Context?
在 Go 1.7 版本之前,context 还是非编制的,它存在于 golang.org/x/net/context 包中。
后来,Golang 团队发现 context 还挺好用的,就把 context 收编了,在 Go 1.7 版本正式纳入了标准库。
Context,也叫上下文,它的接口定义如下
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done()
<p>可以看到 Context 接口共有 4 个方法</p>
Deadline:返回的第一个值是 截止时间,到了这个时间点,Context 会自动触发 Cancel 动作。返回的第二个值是 一个布尔值,true 表示设置了截止时间,false 表示没有设置截止时间,如果没有设置截止时间,就要手动调用 cancel 函数取消 Context。Done:返回一个只读的通道(只有在被cancel后才会返回),类型为struct{}。当这个通道可读时,意味着parent context已经发起了取消请求,根据这个信号,开发者就可以做一些清理动作,退出goroutine。Err:返回 context 被 cancel 的原因。Value:返回被绑定到 Context 的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。
2. 为何需要 Context?
当一个协程(goroutine)开启后,我们是无法强制关闭它的。
常见的关闭协程的原因有如下几种:
- goroutine 自己跑完结束退出
- 主进程crash退出,goroutine 被迫退出
- 通过通道发送信号,引导协程的关闭。
第一种,属于正常关闭,不在今天讨论范围之内。
第二种,属于异常关闭,应当优化代码。
第三种,才是开发者可以手动控制协程的方法,代码示例如下:
func main() {
stop := make(chan bool)
go func() {
for {
select {
case
<p>例子中我们定义一个<code>stop</code>的chan,通知他结束后台goroutine。实现也非常简单,在后台goroutine中,使用select判断<code>stop</code>是否可以接收到值,如果可以接收到,就表示可以退出停止了;如果没有接收到,就会执行<code>default</code>里的监控逻辑,继续监控,只到收到<code>stop</code>的通知。</p>
<p>以上是一个 goroutine 的场景,如果是多个 goroutine ,每个goroutine 底下又开启了多个 goroutine 的场景呢?在 飞雪无情的博客 里关于为何要使用 Context,他是这么说的</p>
<blockquote>chan+select的方式,是比较优雅的结束一个goroutine的方式,不过这种方式也有局限性,如果有很多goroutine都需要控制结束怎么办呢?如果这些goroutine又衍生了其他更多的goroutine怎么办呢?如果一层层的无穷尽的goroutine呢?这就非常复杂了,即使我们定义很多chan也很难解决这个问题,因为goroutine的关系链就导致了这种场景非常复杂。</blockquote>
<p>在这里我不是很赞同他说的话,因为我觉得就算只使用一个通道也能达到控制(取消)多个 goroutine 的目的。下面就用例子来验证一下。</p>
<p>该例子的原理是:使用 close 关闭通道后,如果该通道是无缓冲的,则它会从原来的阻塞变成非阻塞,也就是可读的,只不过读到的会一直是零值,因此根据这个特性就可以判断 拥有该通道的 goroutine 是否要关闭。</p>
<pre class="brush:csharp;">
package main
import (
"fmt"
"time"
)
func monitor(ch chan bool, number int) {
for {
select {
case v := 就说明所有的goroutine都已经关闭
time.Sleep( 5 * time.Second)
fmt.Println("主程序退出!!")
}
输出如下
监控器4,正在监控中...
监控器1,正在监控中...
监控器2,正在监控中...
监控器3,正在监控中...
监控器5,正在监控中...
监控器2,接收到通道值为:false,监控结束。
监控器3,接收到通道值为:false,监控结束。
监控器5,接收到通道值为:false,监控结束。
监控器1,接收到通道值为:false,监控结束。
监控器4,接收到通道值为:false,监控结束。
主程序退出!!
上面的例子,说明当我们定义一个无缓冲通道时,如果要对所有的 goroutine 进行关闭,可以使用 close 关闭通道,然后在所有的 goroutine 里不断检查通道是否关闭(前提你得约定好,该通道你只会进行 close 而不会发送其他数据,否则发送一次数据就会关闭一个goroutine,这样会不符合咱们的预期,所以最好你对这个通道再做一层封装做个限制)来决定是否结束 goroutine。
所以你看到这里,我做为初学者还是没有找到使用 Context 的必然理由,我只能说 Context 是个很好用的东西,使用它方便了我们在处理并发时候的一些问题,但是它并不是不可或缺的。
换句话说,它解决的并不是 能不能 的问题,而是解决 更好用 的问题。
3. 简单使用 Context
如果不使用上面 close 通道的方式,还有没有其他更优雅的方法来实现呢?
有,那就是本文要讲的 Context
我使用 Context 对上面的例子进行了一番改造。
package main
import (
"context"
"fmt"
"time"
)
func monitor(ctx context.Context, number int) {
for {
select {
// 其实可以写成 case 就说明所有的goroutine都已经关闭
time.Sleep( 5 * time.Second)
fmt.Println("主程序退出!!")
}
这里面的关键代码,也就三行
第一行:以 context.Background() 为 parent context 定义一个可取消的 context
ctx, cancel := context.WithCancel(context.Background())
第二行:然后你可以在所有的goroutine 里利用 for + select 搭配来不断检查 ctx.Done() 是否可读,可读就说明该 context 已经取消,你可以清理 goroutine 并退出了。
case
第三行:当你想到取消 context 的时候,只要调用一下 cancel 方法即可。这个 cancel 就是我们在创建 ctx 的时候返回的第二个值。
cancel()
运行结果输出如下。可以发现我们实现了和 close 通道一样的效果。
监控器3,正在监控中...
监控器4,正在监控中...
监控器1,正在监控中...
监控器2,正在监控中...
监控器2,接收到通道值为:{},监控结束。
监控器5,接收到通道值为:{},监控结束。
监控器4,接收到通道值为:{},监控结束。
监控器1,接收到通道值为:{},监控结束。
监控器3,接收到通道值为:{},监控结束。
主程序退出!!
4. 根Context 是什么?
创建 Context 必须要指定一个 父 Context,当我们要创建第一个Context时该怎么办呢?
不用担心,Go 已经帮我们实现了2个,我们代码中最开始都是以这两个内置的context作为最顶层的parent context,衍生出更多的子Context。
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
一个是Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context,它不能被取消。
一个是TODO,如果我们不知道该使用什么Context的时候,可以使用这个,但是实际应用中,暂时还没有使用过这个TODO。
他们两个本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done()
<p><span style="color: #ff0000"><strong>5. Context 的继承衍生</strong></span></p>
<p>上面在定义我们自己的 Context 时,我们使用的是 <code>WithCancel</code> 这个方法。</p>
<p>除它之外,context 包还有其他几个 With 系列的函数</p>
<pre class="brush:plain;">
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。
通过一次继承,就多实现了一个功能,比如使用 WithCancel 函数传入 根context ,就创建出了一个子 context,该子context 相比 父context,就多了一个 cancel context 的功能。
如果此时,我们再以上面的子context(context01)做为父context,并将它做为第一个参数传入WithDeadline函数,获得的子子context(context02),相比子context(context01)而言,又多出了一个超过 deadline 时间后,自动 cancel context 的功能。
接下来我会举例介绍一下这几种 context,其中 WithCancel 在上面已经讲过了,下面就不再举例了
例子 1:WithDeadline
package main
import (
"context"
"fmt"
"time"
)
func monitor(ctx context.Context, number int) {
for {
select {
case
<p>输出如下</p>
<blockquote>
<p>监控器5,正在监控中...<br>
监控器1,正在监控中...<br>
监控器2,正在监控中...<br>
监控器3,正在监控中...<br>
监控器4,正在监控中...<br>
监控器3,监控结束。<br>
监控器4,监控结束。<br>
监控器2,监控结束。<br>
监控器1,监控结束。<br>
监控器5,监控结束。<br>
监控器取消的原因: context deadline exceeded<br>
主程序退出!!</p>
</blockquote>
<p>例子 2:WithTimeout</p>
<p>WithTimeout 和 WithDeadline 使用方法及功能基本一致,都是表示超过一定的时间会自动 cancel context。</p>
<p>唯一不同的地方,我们可以从函数的定义看出</p>
<pre class="brush:plain;">
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithDeadline 传入的第二个参数是 time.Time 类型,它是一个绝对的时间,意思是在什么时间点超时取消。
而 WithTimeout 传入的第二个参数是 time.Duration 类型,它是一个相对的时间,意思是多长时间后超时取消。
package main
import (
"context"
"fmt"
"time"
)
func monitor(ctx context.Context, number int) {
for {
select {
case
<p>输出的结果和上面一样</p>
<blockquote>
<p><span id="1591751956123S" style="display: none"> </span>监控器1,正在监控中...<br>
监控器5,正在监控中...<br>
监控器3,正在监控中...<br>
监控器2,正在监控中...<br>
监控器4,正在监控中...<br>
监控器4,监控结束。<br>
监控器2,监控结束。<br>
监控器5,监控结束。<br>
监控器1,监控结束。<br>
监控器3,监控结束。<br>
监控器取消的原因: context deadline exceeded<br>
主程序退出!!</p>
</blockquote>
<p>例子 3:WithValue</p>
<p>通过Context我们也可以传递一些必须的元数据,这些数据会附加在Context上以供使用。</p>
<p>元数据以 Key-Value 的方式传入,Key 必须有可比性,Value 必须是线程安全的。</p>
<p>还是用上面的例子,以 ctx02 为父 context,再创建一个能携带 value 的ctx03,由于他的父context 是 ctx02,所以 ctx03 也具备超时自动取消的功能。</p>
<pre class="brush:csharp;">
package main
import (
"context"
"fmt"
"time"
)
func monitor(ctx context.Context, number int) {
for {
select {
case
<p>输出如下</p>
<blockquote>
<p>监控器4,正在监控 CPU <br>
监控器5,正在监控 CPU <br>
监控器1,正在监控 CPU <br>
监控器3,正在监控 CPU <br>
监控器2,正在监控 CPU <br>
监控器2,监控结束。<br>
监控器5,监控结束。<br>
监控器3,监控结束。<br>
监控器1,监控结束。<br>
监控器4,监控结束。<br>
监控器取消的原因: context deadline exceeded<br>
主程序退出!!</p>
</blockquote>
<p><span style="color: #ff0000"><strong>6. Context 使用注意事项</strong></span></p>
- 通常 Context 都是做为函数的第一个参数进行传递(规范性做法),并且变量名建议统一叫 ctx
- Context 是线程安全的,可以放心地在多个 goroutine 中使用。
- 当你把 Context 传递给多个 goroutine 使用时,只要执行一次 cancel 操作,所有的 goroutine 就可以收到 取消的信号
- 不要把原本可以由函数参数来传递的变量,交给 Context 的 Value 来传递。
- 当一个函数需要接收一个 Context 时,但是此时你还不知道要传递什么 Context 时,可以先用 context.TODO 来代替,而不要选择传递一个 nil。
- 当一个 Context 被 cancel 时,继承自该 Context 的所有 子 Context 都会被 cancel。
总结
今天关于《深入理解 Go 语言中的 Context》的内容就介绍到这里了,是不是学起来一目了然!想要了解更多关于golang的内容请关注golang学习网公众号!
Go语言Mock使用基本指南详解
- 上一篇
- Go语言Mock使用基本指南详解
- 下一篇
- Go语言json编码驼峰转下划线、下划线转驼峰的实现
-
- Golang · Go教程 | 4小时前 | 格式化输出 printf fmt库 格式化动词 Stringer接口
- Golangfmt库用法与格式化技巧解析
- 140浏览 收藏
-
- Golang · Go教程 | 4小时前 |
- Golang配置Protobuf安装教程
- 147浏览 收藏
-
- Golang · Go教程 | 4小时前 |
- Golang中介者模式实现与通信解耦技巧
- 378浏览 收藏
-
- Golang · Go教程 | 4小时前 |
- Golang多协程通信技巧分享
- 255浏览 收藏
-
- Golang · Go教程 | 5小时前 |
- Golang如何判断变量类型?
- 393浏览 收藏
-
- Golang · Go教程 | 5小时前 |
- Golang云原生微服务实战教程
- 310浏览 收藏
-
- Golang · Go教程 | 5小时前 |
- Golang迭代器与懒加载结合应用
- 110浏览 收藏
-
- Golang · Go教程 | 6小时前 | 性能优化 并发安全 Golangslicemap 预设容量 指针拷贝
- Golangslicemap优化技巧分享
- 412浏览 收藏
-
- Golang · Go教程 | 6小时前 |
- Golang代理模式与访问控制实现解析
- 423浏览 收藏
-
- Golang · Go教程 | 6小时前 |
- Golang事件管理模块实现教程
- 274浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- ChatExcel酷表
- ChatExcel酷表是由北京大学团队打造的Excel聊天机器人,用自然语言操控表格,简化数据处理,告别繁琐操作,提升工作效率!适用于学生、上班族及政府人员。
- 3164次使用
-
- Any绘本
- 探索Any绘本(anypicturebook.com/zh),一款开源免费的AI绘本创作工具,基于Google Gemini与Flux AI模型,让您轻松创作个性化绘本。适用于家庭、教育、创作等多种场景,零门槛,高自由度,技术透明,本地可控。
- 3376次使用
-
- 可赞AI
- 可赞AI,AI驱动的办公可视化智能工具,助您轻松实现文本与可视化元素高效转化。无论是智能文档生成、多格式文本解析,还是一键生成专业图表、脑图、知识卡片,可赞AI都能让信息处理更清晰高效。覆盖数据汇报、会议纪要、内容营销等全场景,大幅提升办公效率,降低专业门槛,是您提升工作效率的得力助手。
- 3405次使用
-
- 星月写作
- 星月写作是国内首款聚焦中文网络小说创作的AI辅助工具,解决网文作者从构思到变现的全流程痛点。AI扫榜、专属模板、全链路适配,助力新人快速上手,资深作者效率倍增。
- 4509次使用
-
- MagicLight
- MagicLight.ai是全球首款叙事驱动型AI动画视频创作平台,专注于解决从故事想法到完整动画的全流程痛点。它通过自研AI模型,保障角色、风格、场景高度一致性,让零动画经验者也能高效产出专业级叙事内容。广泛适用于独立创作者、动画工作室、教育机构及企业营销,助您轻松实现创意落地与商业化。
- 3785次使用
-
- 一篇文章搞懂Go语言中的Context
- 2023-01-01 418浏览
-
- 优雅使用GoFrame共享变量Context示例详解
- 2023-01-18 401浏览
-
- Go语言上下文context底层原理
- 2023-01-22 175浏览
-
- golang的协程上下文的具体使用
- 2022-12-24 236浏览
-
- Go中groutine通信与context控制实例详解
- 2022-12-29 137浏览

