转载地址:https://www.cnblogs.com/chenqionghe/p/14107790.html

一、Context包到底是干嘛用的

我们会在用到很多东西的时候都看到context的影子,比如gin框架,比如grpc,这东西到底是做啥的?
大家都在用,没几个知道这是干嘛的,知其然而不知其所以然

谁都在CRUD,谁都觉得if else就完了,有代码能copy我也行,原理啥啥不懂不重要,反正就是一把梭

原理说白了就是:

  1. 当前协程取消了,可以通知所有由它创建的子协程退出
  2. 当前协程取消了,不会影响到创建它的父级协程的状态
  3. 扩展了额外的功能:超时取消、定时取消、可以和子协程共享数据

二、主协程退出通知子协程示例演示

主协程通知子协程退出

如下代码展示了,通过一个叫done的channel通道达到了这样的效果

package mainimport ("fmt""time"
)func main() {done := make(chan string)//缓冲通道预先放置10个消息messages := make(chan int, 10)defer close(messages)for i := 0; i < 10; i++ {messages <- i}//启动3个子协程消费messages消息for i := 1; i <= 3; i++ {go child(i, done, messages)}time.Sleep(3 * time.Second) //等待子协程接收一半的消息close(done) //结束前通知子协程time.Sleep(2 * time.Second) //等待所有的子协程输出fmt.Println("主协程结束")
}//从messages通道获取信息,当收到结束信号的时候不再接收
func child(i int, done <-chan string, messages <-chan int) {
Consume:for {time.Sleep(1 * time.Second)select {case <-done:fmt.Printf("[%d]被主协程通知结束...\n", i)break Consumedefault:fmt.Printf("[%d]接收消息: %d\n", i, <-messages)}}
}

运行结束如下

这里,我们用一个channel的关闭做到了通知所有的消费到一半的子协程退出。
问题来了,如果子协程又要启动它的子协程,这可咋整?

主协程通知有子协程,子协程又有多个子协程

这是个哲学问题,我们还是得建立一个叫done的channel来监测
下面演示一下这种操作,再在每个child方法里启动多个job,如下

全量代码贴出来

package mainimport ("fmt""time"
)func main() {done := make(chan string)//缓冲通道预先放置10个消息messages := make(chan int, 10)defer close(messages)for i := 0; i < 10; i++ {messages <- i}//启动3个子协程消费messages消息for i := 1; i <= 3; i++ {go child(i, done, messages)}time.Sleep(3 * time.Second) //等待子协程接收一半的消息close(done) //结束前通知子协程time.Sleep(2 * time.Second) //等待所有的子协程输出fmt.Println("主协程结束")
}//从messages通道获取信息,当收到结束信号的时候不再接收
func child(i int, done <-chan string, messages <-chan int) {newDone := make(chan string)defer close(newDone)go childJob(i, "a", newDone)go childJob(i, "b", newDone)Consume:for {time.Sleep(1 * time.Second)select {case <-done:fmt.Printf("[%d]被主协程通知结束...\n", i)break Consumedefault:fmt.Printf("[%d]接收消息: %d\n", i, <-messages)}}
}//任务
func childJob(parent int, name string, done <-chan string) {for {time.Sleep(1 * time.Second)select {case <-done:fmt.Printf("[%d-%v]被结束...\n", parent, name)returndefault:fmt.Printf("[%d-%v]执行\n", parent, name)}}
}

运行结果如下

问题来了,如果job里再启动自己的goroutine,这样没完没了的建立done的通道有点恶心,这时候context包就来了!

我们先把上面的代码改成context包的方式

package mainimport ("context""fmt""time"
)func main() {ctx, cancel := context.WithCancel(context.Background())//缓冲通道预先放置10个消息messages := make(chan int, 10)defer close(messages)for i := 0; i < 10; i++ {messages <- i}//启动3个子协程消费messages消息for i := 1; i <= 3; i++ {go child(i, ctx, messages)}time.Sleep(3 * time.Second) //等待子协程接收一半的消息cancel() //结束前通知子协程time.Sleep(2 * time.Second) //等待所有的子协程输出fmt.Println("主协程结束")
}//从messages通道获取信息,当收到结束信号的时候不再接收
func child(i int, ctx context.Context, messages <-chan int) {//基于父级的context建立contextnewCtx, _ := context.WithCancel(ctx)go childJob(i, "a", newCtx)go childJob(i, "b", newCtx)Consume:for {time.Sleep(1 * time.Second)select {case <-ctx.Done():fmt.Printf("[%d]被主协程通知结束...\n", i)break Consumedefault:fmt.Printf("[%d]接收消息: %d\n", i, <-messages)}}
}//任务
func childJob(parent int, name string, ctx context.Context) {for {time.Sleep(1 * time.Second)select {case <-ctx.Done():fmt.Printf("[%d-%v]被结束...\n", parent, name)returndefault:fmt.Printf("[%d-%v]执行\n", parent, name)}}
}

运行结果如下

可以看到,改成context包还是顺利的通过子协程退出了
主要修改了几个地方,再ctx向下传递

基于上层context再构建当前层级的context

监听context的退出信号,

这就是context包的核心原理,链式传递context,基于context构造新的context

三、Context包的核心接口和方法

更多资料可以查看:Go语言设计与实现

context接口

context是一个接口,主要包含以下4个方法

  • Deadline
    返回当前context任务被取消的时间,没有设定返回ok返回false
  • Done
    当绑定当前的context任务被取消时,将返回一个关闭的channel
  • Err
    Done返回的channel没有关闭,返回nil;
    Done返回的channel已经关闭,返回非空值表示任务结束的原因;
    context被取消,返回Canceled。
    context超时,DeadlineExceeded
  • Value
    返回context存储的键

emptyCtx结构体

实现了context接口,emptyCtx没有超时时间,不能取消,也不能存储额外信息,所以emptyCtx用来做根节点,一般用Background和TODO来初始化emptyCtx

Backgroud

通常用于主函数,初始化以及测试,作为顶层的context

TODO

不确定使用什么用context的时候才会使用

valueCtx结构体

type valueCtx struct{ Context key, val interface{} }

valueCtx利用Context的变量来表示父节点context,所以当前context继承了父context的所有信息
valueCtx还可以存储键值。

Value

func (c *valueCtx) Value(key interface{}) interface{} {if c.key == key {return c.val}return c.Context.Value(key)
}

可以用来获取当前context和所有的父节点存储的key

如果当前的context不存在需要的key,会沿着context链向上寻找key对应的值,直到根节点

WithValue

可以向context添加键值

func WithValue(parent Context, key, val interface{}) Context {if key == nil {panic("nil key")}if !reflect.TypeOf(key).Comparable() {panic("key is not comparable")}return &valueCtx{parent, key, val}
}

添加键值会返回创建一个新的valueCtx子节点

示例

package mainimport ("context""fmt""time"
)func main() {ctx := context.WithValue(context.Background(), "top", "root")//第一层go func(parent context.Context) {ctx = context.WithValue(parent, "cqh", "chenqionghe")//第二层go func(parent context.Context) {ctx = context.WithValue(parent, "xsfz", "雪山飞猪")//第三层go func(parent context.Context) {//可以获取所有的父类的值fmt.Println(ctx.Value("top"))fmt.Println(ctx.Value("cqh"))fmt.Println(ctx.Value("xsfz"))//不存在fmt.Println(ctx.Value("xxxx"))}(ctx)}(ctx)}(ctx)time.Sleep(1 * time.Second)fmt.Println("end")
}

运行结果

可以看到,子context是可以获取所有父级设置过的key

cancelCtx结构体

type cancelCtx struct {Contextmu sync.Mutexdone chan struct{}children map[canceler]struct{}err error
}
type canceler interface {cancel(removeFromParent bool, err error)Done() <-chan struct{}
}

和valueCtx类似,有一个context做为父节点,
变量done表示一个channel,用来表示传递关闭;
children表示一个map,存储了当前context节点为下的子节点
err用来存储错误信息表示任务结束的原因

WithCancel

用来创建一个可取消的context,返回一个context和一个CancelFunc,调用CancelFunc可以触发cancel操作。

示例

package mainimport ("context""fmt""time"
)func main() {ctx, cancel := context.WithCancel(context.Background())//第一层go func(parent context.Context) {ctx, _ := context.WithCancel(parent)//第二层go func(parent context.Context) {ctx, _ := context.WithCancel(parent)//第三层go func(parent context.Context) {waitCancel(ctx, 3)}(ctx)waitCancel(ctx, 2)}(ctx)waitCancel(ctx, 1)}(ctx)time.Sleep(5 * time.Second)cancel()time.Sleep(1 * time.Second)
}func waitCancel(ctx context.Context, i int) {for {time.Sleep(time.Second)select {case <-ctx.Done():fmt.Printf("%d end\n", i)returndefault:fmt.Printf("%d do\n", i)}}
}

运行结果

可以看到,在外边调用cancel方法,所有的子goroutine都已经收到停止信号

timerCtx结构体

timerCtx是基于cancelCtx的context精英,是一种可以定时取消的context,过期时间的deadline不晚于所设置的时间d

WithDeadline

返回一个基于parent的可取消的context,并且过期时间deadline不晚于所设置时间d

WithTimeout

创建一个定时取消context,和WithDeadline差不多,WithTimeout是相对时间

示例

package mainimport ("context""fmt""time"
)func main() {ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)//第一层go func(parent context.Context) {ctx, _ := context.WithCancel(parent)//第二层go func(parent context.Context) {ctx, _ := context.WithCancel(parent)//第三层go func(parent context.Context) {waitCancel(ctx, 3)}(ctx)waitCancel(ctx, 2)}(ctx)waitCancel(ctx, 1)}(ctx)<-ctx.Done()time.Sleep(1 * time.Second)
}func waitCancel(ctx context.Context, i int) {for {time.Sleep(time.Second)select {case <-ctx.Done():fmt.Printf("%d end\n", i)returndefault:fmt.Printf("%d do\n", i)}}
}

运行结果

可以看到,虽然我们没有调用cancel方法,5秒后自动调用了,所有的子goroutine都已经收到停止信号

四、总结核心原理

  1. Done方法返回一个channel
  2. 外部通过调用<-channel监听cancel方法
  3. cancel方法会调用close(channel)
    当调用close方法的时候,所有的channel再次从通道获取内容,会返回零值和false
res,ok := <-done:
  1. 过期自动取消,使用了time.AfterFunc方法,到时调用cancel方法
  c.timer = time.AfterFunc(dur, func() {c.cancel(true, DeadlineExceeded)})

授人以渔不如授人以渔,知其然也知其所以然,让我们共同构建美丽新世界,让人与自然更加和谐,就是这样,giao~

Go语言的context包从放弃到入门相关推荐

  1. 深度解密Go语言之context

    文章目录 什么是 context 为什么有 context context 底层实现原理 整体概览 接口 Context canceler 结构体 emptyCtx cancelCtx timerCt ...

  2. 源码分析 | 深度解密Go语言之context

    之前写的文章 Context是怎么在Go语言中发挥关键作用的 以图解的方式给大家讲解了 Context的实现原理以及它为什么能便捷地对多层并发任务进行控制,写作期间阅读了不少作者的源码解析文章,桃花源 ...

  3. Golang Context包的使用

    context包主要用来控制goroutings间的并发控制.使用场景包括通知子协程退出这种. 相关接口和示例: func Background() Context Background return ...

  4. 深入理解Golang中的Context包

    context.Context是Go语言中独特的设计,在其他编程语言中我们很少见到类似的概念.context.Context深度支持Golang的高并发. 1. Goroutine和Channel 在 ...

  5. 聊城大学c语言实验报告,c语言程序设计(包云)c第1章概述.ppt

    c语言程序设计(包云)c第1章概述.ppt C语言程序设计,讲授包云 单位聊城大学计算机学院,第1章 C语言概述,3,主要内容,1.1 什么是计算机程序 1.2 什么是计算机语言 1.3 C语言的发展 ...

  6. Go语言之 Context 实战与源码分析

    来自:指月 https://www.lixueduan.com 原文:https://www.lixueduan.com/post/go/context/ 本文主要简单介绍了Go语言(golang)中 ...

  7. R语言---下载R包提示00LOCK-dplyr解决

    R语言-下载R包提示00LOCK-dplyr解决 1.遇到问题(R版本3.5.1) 在使用其他软件的时候会调用R包dplyr,当时提示这个软件包版本不够,会影响其他软件的使用.因此需要升级这个R包,当 ...

  8. 一文搞懂Go标准库context包

    "Gopher部落"星球双11现金优惠,点击链接领劵 https://t.zsxq.com/078E1QTjM  立减88元. 自从context包在Go 1.7版本[1]加入Go ...

  9. 三线表是什么?R语言使用table1包绘制(生成)三线表、构建不分层的三线表

    三线表是什么?R语言使用table1包绘制(生成)三线表.构建不分层的三线表 目录

最新文章

  1. ant引入html页面,antd引入普通html使用,将ant Design本地化
  2. python自己的模块_Python--构建发布自己的模块
  3. java的ZipOutputStream压缩文件的两个问题(乱码和每次zip后文件md5变化)
  4. A Concise and Provably Informative Multi-Scale Signature Based on Heat Diffusion
  5. php如何获取js中的内容_解析PHP中的Javascript提取
  6. Android5.0 netd架构流程
  7. 如何判断 linux内核 中 如何判断 条件编译
  8. bzoj2655 calc
  9. 十大常用算法之马踏棋盘算法
  10. 如何卸载2345soft文件夹
  11. 【Android -- 开源库】BRVAH 的基本使用
  12. 信息系统项目管理师 - 项目沟通管理
  13. CT重建学习笔记(一)
  14. 【题解】HNOI-2015落忆枫音
  15. 植物大战僵尸阳光金币修改器(外g)c++代码实现
  16. A The Miracle and the Sleeper
  17. cadence SPB17.4 - allegro DRC - Physical - Maximum Neck Length
  18. 【汽车安全】ISO26262概要
  19. html+复制插件,jenkins 插件Copy Artifacts + Artifacts to copy
  20. Understand安装与使用

热门文章

  1. java.nio.DirectByteBuffer管理堆外内存
  2. 快速排序思路(Hoare版),代码实现
  3. python统计词频_python统计词频
  4. 小程序swiper怎么让内容撑开高度_[视频]微信小程序实战优购商城,涵盖你所学的技能点...
  5. MySQL 示例数据库 employees 详解
  6. CCNA2.0笔记_ACL
  7. 生产环境使用elasticsearch遇到的一些问题以及解决方法(不断更新)
  8. 关于垂直切分Vertical Sharding的粒度
  9. Objective C的那点小语法
  10. drawRect方法在UIImageView的派生类中不被调用