[Golang]正确使用Context|世界今热点
01 为什么要引入Context
context.Context是Go中定义的一个接口类型,从1.7版本中开始引入。其主要作用是在一次请求经过的所有协程或函数间传递取消信号及共享数据,以达到父协程对子协程的管理和控制的目的。
需要注意的是context.Context的作用范围是一次请求的生命周期,即随着请求的产生而产生,随着本次请求的结束而结束。如图所示:
(资料图片)
02 什么是context.Context
在context包中,我们看到context.Context的定义实际上是一个接口类型,该接口定义了获取上下文的Deadline的函数,根据key获取value值的函数、还有获取done通道的函数。如下:
typeContextinterface{Deadline()(deadlinetime.Time,okbool)Done()<-chanstruct{}Err()error Value(key interface{}) interface{}}
由定义的接口函数可知,对于传递取消信号的行为我们可以描述为:当协程运行时间达到Deadline时,就会调用取消函数,关闭done通道,往done通道中输入一个空结构体消息struct{}{},这时所有监听done通道的子协程都会收到该消息,便知道父协程已经关闭,需要自己也结束运行。
下面是一个使用Context的简易示例,我们通过该示例来说明父子协程之间是如何传递取消信号的。
func main() { ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second) defer cancel() go doSomethingCool(ctx) select { case <-ctx.Done(): fmt.Println("oh no, I"ve exceeded the deadline") }}func doSomethingCool(ctx context.Context) { for { select { case <-ctx.Done(): fmt.Println("timed out") return default: fmt.Println("doing something cool") } time.Sleep(500 * time.Millisecond) }}
由示例可知,main协程和doSomething函数之间的唯一关联就是ctx.Done()。当子协程从ctx.Done()通道中接收到输出时(因为超时自动取消或主动调用了cancel函数),即认为是父协程不再需要子协程返回的结果了,子协程就会直接返回,不再执行其他的逻辑。
03 Context的作用一:协程间传递信号
3.1 如何创建带可以传递信号的Context
在开头处我们得知Context本质是一个接口类型。接口类型是需要具体的结构体起来实现的。那我们需要自定义结构体类型来实现这些接口吗?答案是不需要。因为在context包中已经定义好了所需场景的结构体,这些结构体已经帮我们实现了Context接口的方法,在项目中就已经够用了。
在context包中定义有emptyCtx、cancelCtx、timerCtx、valueCtx
四种结构体。其中cancelCtx、timerCtx实现了给子协程传递取消信号。valueCtx结构体实现了父协程和子协程传递共享数据相关。本节我们重点来看跟传递信号相关的Context。
在上面示例中,我们通过context.WithTimeout函数创建了一个带定时取消功能的Context实例,该示例本质上是创建了一个timerCtx结构体的实例。在context包中还有WithCancel、WithDeadline函数也可以创建对应的结构体,其定义如下:
//创建带有取消功能的Contextfunc WithCancel(parent Context) (ctx Context, cancel CancelFunc) //创建带有定时自动取消功能的Contextfunc WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)//创建带有定时自动取消功能的Contextfunc WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
对应的函数创建的结构体及该实例所实现的功能的主要特点如下图所示:
在图中我们看到结构体依次是继承关系。因为在cancelCtx结构体内嵌套了Context(实际上是emptyCtx)、timerCtx结构体内嵌套了cancelCtx结构体,可以认为他们之间存在继承关系。
通过WithTimeout和WithDealine函数创建的Context实际上都是timerCtx结构体,唯一的区别就是WithDeadline函数的第二个参数指定的是最后的时间点,而WithTimeout函数的第二个参数是一段时间。但WithDealine在内部实现中本质上也是将时间点转换成距离当前的时间段。
3.2 为什么Done函数返回值是通道
在Context接口的定义中我们看到Done函数的定义,其返回值是一个输出通道:
Done() <-chan struct{}
在上面的示例中我们看到的子协程是通过监听Context的Done()函数返回的通道来判断父协程是否发送了取消信号的。当父协程调用取消函数时,该取消函数将该通道关闭。关闭通道相当于是一个广播信息,当监听该通道的接收者从通道到中接收完最后一个元素后,接收者都会解除阻塞,并从通道中接收到通道元素类型的零值。
既然父子协程是通过通道传到信号的。下面我们介绍父协程是如何将信号通过通道传递给子协程的。
3.3 父协程是如何取消子协程的
我们发现在Context接口中并没有定义Cancel方法。实际上通过WithCancel函数创建的一个具有可取消功能的Context实例来实现的:
// WithCancel returns a copy of parent whose Done channel is closed as soon as// parent.Done is closed or cancel is called.func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { if parent == nil { panic("cannot create context from nil parent") } c := newCancelCtx(parent) propagateCancel(parent, &c) return &c, func() { c.cancel(true, Canceled) }}
WithCancel函数的返回值有两个,一个是ctx,一个是取消函数cancel。当父协程调用cancel函数时,就相当于触发了关闭的动作,在cancel的执行逻辑中会将ctx的done通道关闭,然后所有监听该通道的子协程就会收到一个struct{}类型的零值,子协程根据此便执行了返回操作。下面是cancel函数实现:
// cancel closes c.done, cancels each of c"s children, and, if// removeFromParent is true, removes c from its parent"s children.func (c *cancelCtx) cancel(removeFromParent bool, err error) { //... d, _ := c.done.Load().(chan struct{})//获取通道 if d == nil { c.done.Store(closedchan) } else { close(d) //关闭通道done } //...}
由源码可知,cancelCtx的cancel函数执行时会关闭通道close(d)。
通过WithCancel函数构造的Context,需要开发者自己设定调用取消函数的条件。而在某些场景下需要设定超时时间,比如调用grpc服务时设置超时时间,那么实际上就是在构造Context的同时,启动一个定时任务,当达到设定的定时时间时,就自动调用cancel函数即可。这就是context包中提供的WithDeadline和WithTimeout函数来构造的上下文。如下是WithDeadline函数的关键实现部分:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { //... c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) //... if c.err == nil { //这里实现定时器,即dur时间后执行cancel函数 c.timer = time.AfterFunc(dur, func() { c.cancel(true, DeadlineExceeded) }) } return c, func() { c.cancel(true, Canceled) }}
WithTimeout函数也是将相对时间timeout转换成绝对的时间点deadline之后,调用的WithDeadline函数。
3.4为什么要通过WithXXX函数构造一个树形结构
很多文章都说,通过WithXXX函数基于Context会衍生出一个Context树,树的每个节点都可以有任意多个子节点Context。如下图表示:
那为什么要构造一个树形结构呢?我们从处理一个请求时经过的多个协程来角度来理解会更容易一些。当一个请求到来时,该请求会经过很多个协程的处理,而这些协程之间的关系实际上就组成了一个树形结构。如下图:
Context的目的就是为了在关联的协程间传递信号和共享数据的,而每个协程又只能管理自己的子节点,而不能管理父节点。所以,在整个处理过程中,Context自然就衍生成了树形结构。
3.5为什么WithXXX函数返回的是一个新的Context对象
通过WithXXX的源码可以看到,每个衍生函数返回来的都是一个新的Context对象,并且都是基于parent Context的。以WithDeadline为例,就是返回的一个timerCtx新的结构体实例。这是因为,在Context的传递过程中,每个协程都能根据自己的需要来定制Context(例如,在上图中,main协程调用goroutine2时要求是600毫秒完成操作,但goroutine2调用goroutine2.1时,要求是500毫秒内完成操作),而这些修改又不能影响之前已经调用的函数,只能对向下传递。所以,通过一个新的Context值来进行传递。
04 Context的作用二:协程间共享数据
Context的另外一个功能就是在协程间共享数据。该功能是通过WithValue函数构造的Context来实现的。我们看下WithValue的实现:
func WithValue(parent Context, key, val interface{}) Context { if parent == nil { panic("cannot create context from nil parent") } if key == nil { panic("nil key") } if !reflectlite.TypeOf(key).Comparable() { panic("key is not comparable") } return &valueCtx{parent, key, val}}
实现代码很简短,我们看到最终返回的是一个valueCtx结构体实例。其中有两点:一是key的类型必须是可比较的。二是value是不能修改的,即具有不可变性。如果需要添加新的值,只能通过WithValue基于原有的Context再生成一个新的valueCtx来携带新的key-value。这也是Context的值在传递过程中是并发安全的原因。从另外一个角度来说,在获取一个key的值的时候,也是递归的一层一层的从下往上查找,如下:
func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key)}
上面简单介绍了下在协程间调用的时候是如何通过Context共享数据的。
但这里讨论的重点是什么样的数据需要通过Context来共享,而不是通过传参的方式。总结下来有以下两点:
携带的数据作用域必须是在请求范围内有效的。即该数据随着请求的产生而产生,随着请求的结束而结束,不会永久的保存。携带的数据不建议是关键参数,关键参数应显式的通过参数来传递。例如像trace_id之类的,用于维护作用,就适合用在Context中传递。4.1 什么是请求范围(request-scoped)内的数据
这个没有一个明显的划定标准。一般的请求范围的数据就是用来表示该请求的元数据。比如该请求是由谁发出(即user id),该请求是在哪儿发出的(即user ip,请求是从该用户的ip位置发出的)。
例如,如果一个日志对象logger是一个单例那么它也不是一个请求范围内的数据。但如果该logger包含了发送请求的来源信息,以及该请求是否启动了调试功能的开关信息,那么该logger也可以被认为是一个请求范围内的数据。
4.2 使用Context.Value的缺点
使用Context.Value会对降低函数的可读性和表达性。例如,下面是使用Context.Value来携带token验证角色的示例:
func IsAdminUser(ctx context.Context) bool { x := token.GetToken(ctx) userObject := auth.AuthenticateToken(x) return userObject.IsAdmin() || userObject.IsRoot()}
当用户调用该函数的时候,仅仅知道该函数带有一个Context类型的参数。但如果要判断一个用户是否是Admin必须要两部分要说明:一个是验证过的token,一个是认证服务。
我们将该函数的Context移除,然后使用参数的方式来重构,如下:
func IsAdminUser(token string, authService AuthService) bool { x := token.GetToken(ctx) userObject := auth.AuthenticateToken(x) return userObject.IsAdmin() || userObject.IsRoot()}
那么这个函数的可读性和表达性就比重构前提高了很多。调用者通过函数签名就很容易知道要判断一个用户是否是AdminUser,只需要传入token和认证的服务authService即可。
4.3 context.Value的使用场景
一般复杂的项目都会有中间件层以及大量的抽象层。如果将类似token或userid这样简单的参数以参数的方式从第一个函数层层传递,那对调用者来说将会是一种噩梦。如果将这样的元数据通过Context来携带进行传递,将会是比较好的方式。在实际项目中,最常用的就是在中间件中。我们以iris为web框架,来看下在中间件中的应用:
package mainimport ( "context" "github.com/google/uuid" "github.com/kataras/iris/v12")func main() { app := iris.New() app.Use(RequestIDMiddleware) app.Get("/hello", mainHandler) app.Listen("localhost:8080", iris.WithOptimizations)}func RequestIDMiddleware(c iris.Context) { reqID := uuid.New() ctx := context.WithValue(c.Request().Context(), "req_id", reqID) req := c.Request().Clone(ctx) c.ResetRequest(req) c.Next()}func mainHandler(ctx iris.Context) { req_id := ctx.Request().Context().Value("req_id") ctx.Writef("Hello request id:%s", req_id) return}
05 总结
context包是go语言中的一个重要的特性。要想正确的在项目中使用context,理解其背后的工作机制以及设计意图是非常重要的。context包定义了一个API,它提供对截止日期、取消信号和请求范围值的支持,这些值可以跨API以及在Goroutine之间传递。
标签:
推荐
- [Golang]正确使用Context|世界今热点
- 【环球快播报】维康药业:3月13日获融资买入330.20万元,占当日流入资金比例23.08%
- 世界快资讯丨马蜂蛰了怎么办好_马蜂蛰了应该怎么办
- 世界实时:德阳新型冠状病毒肺炎疫情:3月14日德阳疫情最新消息今天数据统计情况通报
- 英国文学史3_关于英国文学史3的简介_天天快消息
- 陈梦2-3罗马尼亚老将萨马拉,无缘WTT大满贯第三轮 当前讯息
- 电脑怎么清除缓存_电脑清除缓存的方法_环球实时
- 3月13日中药板块涨幅达2%
- 打卡介苗后几天会出现反应_打卡介苗后的反应
- 资产系统工作计划制定思路(必备6篇)
- 天天快报!肾积水有什么危害_肾积水有哪些症状
- 2023年短道速滑世锦赛收官 中国队获1金1银
- 美少女战士剧场版_关于美少女战士剧场版的介绍
- 焦点报道:龙珠z卡卡罗特修改器风灵月影
- 每日焦点!脚尖滑过的地方仿写怎么写_脚尖滑过的地方仿写句子
- 世界要闻:什么是国学_国学简述
- 世界通讯!pest公共英语等级考试_pest英语考试官网
- 皮下脂肪瘤长什么样图片_脂肪瘤长什么样图片 世界微动态
- 陶哲轩:ChatGPT已加入我的数学工作流 焦点消息
- 大通县气象台发布道路结冰黄色预警信号|今日热门
- 大学后面是什么学位排序_大学后面是什么学位
- 世界最资讯丨胡萝卜榨汁是生的好还是煮熟?
- 德玛西亚第四季 环球观速讯
- 女人分居多久容易出轨_三种情况女人必出轨
- 贵州一房东想涨房租后,租客搬到对面去租房,房东还来纠缠不清
- 世界观天下!人法网征信查询系统_人法道
- 世界速看:Mysteel周报:湖南省钢材价格上涨 成交有所放量
- 当前信息:埃弗里布拉德利个人资料(埃弗里家族)
- 天地者万物之逆旅是什么意思_天地者万物之逆旅
- 深圳打出“组合拳” 促外贸稳定发展 世界新消息
- 互操互爱一家人txt_红杏泄春光txt-环球快播报
- 帕萨特1.8t真实油耗是多少呢_帕萨特1.8t真实油耗
- 【天天热闻】拍游戏视频用什么软件_拍游戏视频用什么软件
- 全球热推荐:28500大写怎么写-285800大写
- 吕布奉先的炫彩皮肤怎么得(吕布奉先)-世界头条
- 全球观察:finereport报表开发_finereport报表
- 头条焦点:阳信县金阳街道举办城镇公益岗庆祝妇女节趣味活动
- 世界快播:CBA全明星星锐赛:崔永熙林葳邹阳陈国豪出战
- 土耳其的首都是伊斯坦布尔吗_土耳其首都伊斯坦布尔被称为______ 世界观热点
- 环球热点评!红色的石头有哪几种_红色有哪几种
- 创纪录!吴易昺获中国大陆男网海外大师赛正赛首胜
- 全球热点评!什么是帕斯卡原理_静压传递原理_什么是帕斯卡原理
- 绝世唐门王冬的身世_绝世唐门王东|天天快资讯
- 三星Galaxy Note 10 Lite泄漏的渲染揭示颜色选项 支持页面上线了
- 焦点速递!西宁市城北区:送戏下乡情暖“三八” 文化惠民共促繁荣
- 【世界报资讯】汽车资讯:全新的宝马4系列敞篷车已经被窥探在其最具启发性的伪装
- 2019西安大唐不夜城着火原因_大唐不夜城着火还能去旅行吗
- 3月停售,一代经典退出中国!华系的成功,扯掉合资车“遮羞布”
- 6红球赛:周跃龙2-5塔猜亚无缘出线 马海龙不敌小特出局
- 建筑凿毛有什么用_地面凿毛是什么意思
- 飞马国际:骏马环保为持股型管理平台,其主要的经营实体为下属子公司大同富乔
- 绝地求生对显卡的最低要求_绝地求生显卡最低要求-环球热头条
- 快资讯:保障工厂生产不受影响 武汉天然气周末凌晨排故障
- 当前滚动:请问采购员的简历应怎样写呢_请问采购员的简历应怎样写
- 星瞳-环球最新
- 每日焦点!激光溶脂和吸脂的区别有哪些_哪种方式对身体伤害少
- 艾沐茵的功效 艾沐茵成分 世界简讯
- 快看:形容迪士尼的成语_尼的成语
- 普通住宅
- 360云盘收费
- 恐惧之魂 天天讯息
- 全球消息!鳄龟怎么养才能爆刺_鳄龟怎么养
- pdf文件怎么做目录链接(pdf文件怎么做)
- 不二 冯唐|天天观点
- 今日热搜:道路“破相”先别慌 社区赶紧来“补妆”
- 住建部部长:对房地产市场企稳回升充满信心|报资讯
- 每日报道:利川属于湖北省哪个市
- 攀岩技巧与训练_攀岩技巧 世界微速讯
- 上海青年旅社-热文
- 阳了应该怎么办-阳性感染者如何用药 03月07日14时安徽安庆疫情数据
- 每日速读!洞口县木瓜中学教师彭玲香心系特教孩子
- 当前要闻:导播台
- 【世界新视野】百度智能云携手济南二机床打造高端装备“云端智造”
- 黑暗传的内容梗概-视点
- 光大证券(601788)3月6日主力资金净卖出1.07亿元
- 快播:什么是日偏食和日环食_什么是日偏食
- 中国四大民间传说故事是哪四个_中国四大民间传说
- 甘英出使西域_甘英
- 短讯!李银河作品
- 世界时讯:龙纹身的女孩精彩片段_龙纹身的女孩3
- 全球时讯:华晨宇个人资料介绍身高体重_华晨宇个人资料介绍
- 全国人大代表陈燕:将快递员等新就业形态劳动者纳入工伤保险范围 进一步明确非标准劳动关系认定方法和标准
- 西郊线_天天观点
- 世界今日讯!广弘控股(000529)3月6日主力资金净买入288.98万元
- 世界热消息:admire怎么读_admin怎么读
- 姐姐结婚送什么礼物比较有意义_姐姐结婚送什么礼物 每日报道
- 环球今日讯!低碳水化合物午餐创意
- 天天精选!时代中国控股2023年前2个月累计合同销售额约28.13亿元
- 鼠标右键失灵怎么修复_鼠标右键失灵该怎么办 天天快报
- 压榨油和浸出油的区别哪种健康_浸出油和压榨油有什么区别-世界速看
- 环球微头条丨组图|江苏泰州:植绿护绿 美化家园
- 大年初四可以洗澡吗
- 科大讯飞董事长刘庆峰:鼓励产业基金参照OpenAI和微软投资新模式
- 【天天播资讯】父亲节几月几号
- 蔡光洁委员:加强中国书法教育普及,增强文化自信
- 百通物流app_百通物流
- 民工调研第2期:中信保诚基金江峰
- 百事通!山西阳泉旅游景点
- 东风本田全新INSPIRE正式宣布中文名“石英派”
- ccd是什么意思_ccd是什么|每日热闻
X 关闭
行业规章
X 关闭