关键词:
前言
首先解答上一篇文章一文带你快速入门context
中留下的疑惑,为什么要defer cancelFunc()
?
func main() {
parent := context.Background()
for i := 0; i < 100; i++ {
go doRequest(parent)
}
time.Sleep(time.Second * 10)
}
// doRequest 模拟网络请求
func doRequest(parent context.Context) {
ctx, _ := context.WithTimeout(parent, time.Second*5)
time.Sleep(time.Millisecond * 200)
go func() {
<-ctx.Done()
fmt.Println("ctx done!")
}()
}
看上面的代码,在main
函数中异步调用doRequest
函数,doRequest
函数中新建一个5s
超时的上下文,doRequest
函数的调用时长为200ms
<img src="https://images-1255831004.cos.ap-guangzhou.myqcloud.com/halo/2021-07-10-142940.png" style="zoom:50%;" />
可以看到,doRequest
的上下文时间范围远大于函数调用花费的时间,在函数结束后没有主动取消上下文,这会造成上下文泄露
所以,defer cancelFunc()
的目的是避免上下文泄露!!
主动调用cancelFunc是一个好习惯!
了解一下Context接口
type Context interface {
// [1] 返回上下文的截止时间
Deadline() (deadline time.Time, ok bool)
// 返回一个通道,当上下文结束时,会关闭该通道,此时 <-ctx.Done() 结束阻塞
Done() <-chan struct{}
// [2] 该方法会在上下文结束时返回一个not nil err,该err用于表示上下文结束的原因
Err() error
// 返回与key关联的上下文的value
Value(key interface{}) interface{}
}
[1]处
,当上下文没有设置截止时间时,调用Deadline
,返回结果值中,ok = false
func main() {
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()
deadline, ok := ctx.Deadline()
fmt.Printf("ok = %v, deadline = %v\n", ok, deadline)
// 输出 ok = false, deadline = 0001-01-01 00:00:00 +0000 UTC
}
[2]处
,即使主动取消上下文,Err
返回值not nil
func main() {
// 设置上下文10s的超时时间
ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second * 10)
go func() {
// 1s后主动取消上下文
<-time.After(time.Second)
cancelFunc()
}()
<-ctx.Done()
err := ctx.Err()
fmt.Printf("err == nil ? %v\n", err == nil)
// 输出 err == nil ? false
}
有几个结构体不能错过
看完Context
接口后,我们来了解一下context
包中预定义的4种上下文
对应的结构体
<img src="https://images-1255831004.cos.ap-guangzhou.myqcloud.com/halo/2021-07-10-151151.png" style="zoom:50%;" />
可以看到,4种上下文分别对应3种结构体,超时上下文和截止时间上下文底层使用都是timerCtx
然后,来看看这3种结构体当中有什么属性,以及它们是如何实现Context
接口
cancelCtx
type cancelCtx struct {
Context // [1] 匿名接口
mu sync.Mutex // 这个锁是用来保护下面这些字段的
done chan struct{} // [2] 这个channel的初始化方式为懒加载
children map[canceler]struct{}
err error
}
// 新建可取消的上下文
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
func (c *cancelCtx) Value(key interface{}) interface{} {
if key == &cancelCtxKey {
return c
}
// 搜索父级上下文的value
return c.Context.Value(key)
}
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
// 懒加载,第一次调用Done方法的时候,channel才初始化
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d
}
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
[1]处
,可以看到cancelCtx
嵌入了一个匿名接口
构建cancelCtx
结构体时,使用父级上下文parent
作为结构体匿名接口的实现
同时结构体中重写了匿名接口中的3个方法,分别是Value
,Done
,Err
所以,当调用cancelCtx
中的Deadline
方法时,实际上是调用parent
的Deadline
方法
[2]处
,结构体中表示上下文结束的done
通道是懒加载的形式初始化,会在首次调用Done
方法的时候,初始化done
通道
timerCtx
type timerCtx struct {
cancelCtx // [1] 内嵌结构体
timer *time.Timer // [2] 用于实现截止时间
deadline time.Time
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
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) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// 当子上下文的截止时间超过父级上下文时,直接构造可取消的上下文并返回
return WithCancel(parent)
}
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
// 定时器,到达截止时间后,结束上下文
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
[1]处
可以看到,timerCtx
内嵌cancelCtx
结构体,所以构建timerCtx
时,也是接受父级上下文parent
作为其内嵌接口的实现,而且timerCtx
只重写Deadline
方法
[2]处
可以看到,上下文的截止时间的控制本质就是通过timer
定时器控制,通过timer.AfterFunc
实现在指定时间cancel
掉上下文
valueCtx
type valueCtx struct {
Context
key, val interface{}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key) // 寻找父级上下文中是否包含与该key关联的值
}
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
// [1] 存入key的类型是不可比较时直接panic
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
valueCtx
整体还是前两个结构体更为简单,父级上下文parent
只重写了Value
方法
主要关注的地方是[1]处
,什么类型是不可比较的?
- slice
- map
- func
这三种类型是不可以比较的,也就是将切片、map或者函数作为valueCtx
的key
是会导致程序panic
的!!
思考以下几个问题
上下文中的通道为什么要懒加载?
我的猜测是节省内存
首先,不管是主动取消还是定时结束上下文,都会调用到cancel
函数
函数中会判断,此时上下文的通道是否为空,如果为空,则使用一个全局变量closedchan
,这个通道是在包初始化阶段就close
掉
// 这是一个可重复使用的通道
var closedchan = make(chan struct{})
func init() {
// 包初始化时,关闭通道
close(closedchan)
}
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
....
if c.done == nil {
// 如果done为空,就代表当前还没调用过Done方法,则直接使用closechan替代
c.done = closedchan
} else {
close(c.done)
}
....
}
上下文的使用不一定都需要调用Context.Done
方法
通过可重复使用的closedchan
,避免了在构建上下文的过程中立马初始化done
通道,减少了一些不必要的内存分配
多次调用cancelFunc会怎么样?
并不会怎么样,多次主动取消上下文不会产生任何错误
调用cancelFunc
时,底层调用cancel
函数,函数中会判断当前上下文是否已经结束,如果已经结束了则直接return
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
c.mu.Lock()
if c.err != nil {
// err不为空,代表上下文已经被取消掉了,直接结束流程
c.mu.Unlock()
return
}
...
}
<img src="https://images-1255831004.cos.ap-guangzhou.myqcloud.com/halo/2021-07-10-164732.jpg" />
当前上下文的截止时间能否超过父级上下文的截止时间?
不能,此时上下文的截止时间会跟父级上下文的截止时间保持一致
可以看到,WithDeadline
函数中,第一步就校验了截止时间
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// 当子上下文的截止时间超过父级上下文时,直接构造可取消的上下文并返回
return WithCancel(parent)
}
....
}
当返回一个可取消的上下文时,表示子上下文的截止时间跟父级上下文是一致的
background和todo的区别是什么?
本质上并没有任何区别,底层都是使用emptyCtx
构造的,主要的区别在于使用语义上
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
当不确定要传什么上下文的时候,就选择TODO
,不过通常这种情况都应该是暂时性的
[万字长文]一文带你深入了解androidgradle(代码片段)
作为每一个Android研发,相信对gradle并不陌生,androidstudio新建每个项目或者module都会自动生成gradle文件,AS默认也是采用Gradle作为构建工具的。但是apk打包背后是如何gradle产生联系,以及gradle还能为我们平时的开发... 查看详情
一文带你深入了解《c语言对齐与非对齐访问》(arm指令集)(代码片段)
首先你需要知道在什么情况下你才需要用到对齐与非对齐这个概念typedefstruct char*pCmd; //4个字节 char*pCmdPara;//4个字节 charisFree; //4个字节 AT_QUEUE_ITEM_T;AT_QUEUE_ITEM_Tq[500]=0;//12*500=6000个字节上面 查看详情
一文带你深入了解《c语言对齐与非对齐访问》(arm指令集)(代码片段)
首先你需要知道在什么情况下你才需要用到对齐与非对齐这个概念typedefstruct char*pCmd; //4个字节 char*pCmdPara;//4个字节 charisFree; //4个字节 AT_QUEUE_ITEM_T;AT_QUEUE_ITEM_Tq[500]=0;//12*500=6000个字节上面 查看详情
薪资界的天花板!一文带你深入原理学习阿里巴巴redis全能笔记
Redis是当下一线互联网企业使用最广泛的存储中间件。Redis发展至今可以说是非常的完善了,但是在当下很多程序员开发过程中使用的Redis锁之类的都是别的架构师封装好的,往往只能停留在了业务层面了。这样肯定是不... 查看详情
都java19了,还不了解java8?一文带你深入了解java8新特性
Java8(又称为jdk1.8)是Java语言开发的一个主要版本。Oracle公司于2014年3月18日发布Java8,它支持函数式编程,新的JavaScript引擎,新的日期API,新的StreamAPI等。(文章很长,建议点赞收藏)新特性以下是Java8新增的部分特性,更多新特... 查看详情
web前端一文带你吃透css(中篇)
前端学习路线小总结:基础入门:HTMLCSSJavaScript三大主流框架:VUEREACTAngular深入学习:小程序NodejQueryTypeScript前端工程化继续学习CSS吧!一.CSS盒子模型(BoxModel)1.CSS盒子模型2.元素的宽度和高度二.CSS边框(Border)1.边框样式 查看详情
web前端一文带你吃透html(下篇)
前端学习路线小总结:基础入门:HTMLCSSJavaScript三大主流框架:VUEREACTAngular深入学习:小程序NodejQueryTypeScript前端工程化一起学习HTML下篇吧!一.HTML区块1.HTML区块元素2.HTML内联元素3.HTML的div元素4.HTML的span元素 查看详情
web前端一文带你吃透css(完结篇)
前端学习路线小总结:基础入门:HTMLCSSJavaScript三大主流框架:VUEREACTAngular深入学习:小程序NodejQueryTypeScript前端工程化文章目录一.CSS布局-对齐1.水平对齐1.1元素居中对齐1.2文本居中对齐1.3图片居中对齐1.4使用定位方式实现左右... 查看详情
一文带你弄懂cdn技术的原理
对于CDN这个东西,相信大家都有耳闻,感觉既陌生但又熟悉。最近深入了解了一下CDN,这才发现原来CDN如此重要!今天就跟大家科普一下CDN是什么,以及为啥要有CDN,最后再讲一下CDN的工作过程!浏览器的网络请求要理解CDN这件... 查看详情
深入理解golang中的context包(代码片段)
context.Context是Go语言中独特的设计,在其他编程语言中我们很少见到类似的概念。context.Context深度支持Golang的高并发。1.Goroutine和Channel在理解context包之前,应该首先熟悉Goroutine和Channel,能加深对context的理解。1.1Gorouti... 查看详情
深入理解golang中的context包(代码片段)
context.Context是Go语言中独特的设计,在其他编程语言中我们很少见到类似的概念。context.Context深度支持Golang的高并发。1.Goroutine和Channel在理解context包之前,应该首先熟悉Goroutine和Channel,能加深对context的理解。1.1Gorouti... 查看详情
♥java枚举实例---交通灯,一文带你深入理解枚举,了解融会贯通javase所该具备的能力java养成(代码片段)
Java学习打卡:第十八天内容导航Java学习打卡:第十八天内容管理基础问题(一定不要忘记)Java对象清除机制(垃圾处理机制)什么样的对象是垃圾呢?那一个对象成为垃圾有哪几种情况?一个良... 查看详情
一文带你了解2018年最流行的前端技术
2018年即将过半,前端开发这个行业又进一个台阶了。找来一个现代前端技术图谱看看,真是吓尿了——宝宝心里苦啊!点图片看大图 仔细想想,这要是全学会了还得了,也太不切实际了。还是来看看现在流行的是有哪些东... 查看详情
web前端一文带你吃透html(上篇)(代码片段)
前端学习路线小总结:基础入门:HTMLCSSJavaScript三大主流框架:VUEREACTAngular深入学习:小程序NodejQueryTypeScript前端工程化🍁开始前端之旅吧!一.HTML简介1.什么是HTML?2.HTML标签3.HTML元素4.HTML版本5.Web浏览器6.HTM... 查看详情
一文带你深入了解《c语言对齐与非对齐访问》(arm指令集)(代码片段)
首先你需要知道在什么情况下你才需要用到对齐与非对齐这个概念typedefstruct char*pCmd; //4个字节 char*pCmdPara;//4个字节 charisFree; //4个字节 AT_QUEUE_ITEM_T;AT_QUEUE_ITEM_Tq[500]=0;//12*500=6000个字节上面的代码 isF... 查看详情
golang为什么需要context(代码片段)
当前go的各种源码中应该都可以看到context的使用,Context是golang1.7的引入的核心结构,本质是为了处理go的并发控制问题。本文主要带大家深入理解context如何使用,为什么需要context和context设计原理。要是本文对您有帮... 查看详情
一文带你掌握java开发利器:maven(代码片段)
Maven如果作为一个Java程序员,那么在日常的开发过程中,maven是很常见的项目构建工具。maven可以极大的提高我们的开发效率,帮助我们简化开发过程中一些解决依赖和项目部署的相关问题,所以学习掌握maven的相... 查看详情
spring入门到精通,一文带你轻松搞定spring!
Spring是一个开放源代码的设计层面框架,他解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。本文章将深入浅出讲解Spring的核心技术IoC、AOP,剖析框架的源代码。让大家快速掌握框... 查看详情