Go Context 最佳实践

发表于 3年以前  | 总阅读数:444 次

去年写了[也许是 Context 最佳实践] , 回头看有些遗漏,重新编辑整理,总结截至 go 1.17 的最佳实践

尽管有人说Context should go away in GO2[1], 但是现有的代码中还是大量使用 Context, 并不是每个人都了解 Context, 从去年到现在就见过两次因为错误使用导致的问题。每个同学都会踩到坑,今天分享下 Context 库使用的 Dos and Don'ts

使用场景

Context 主要有以下三种使用场景

  • 传递超时信息,这点用的最多
  • 传递信号,用于消息通知,处理多协程通信
  • 传递数据,常用的框架层 trace-id, metadata

举一个 etcd watch 的例子,我们加深了解

func watch(ctx context.Context, revision int64) {
 ctx, cancel := context.WithCancel(ctx)
 defer cancel()

 for {
  rch := watcher.Watch(ctx, watchPath, clientv3.WithRev(revision))
  for wresp := range rch {
    ......
      doSomething()
  }

  select {
  case <-ctx.Done():
   // server closed, return
   return
  default:
  }
 }
}

首先基于参数传进来的 parent ctx 生成了 child ctxcancel 函数。然后 Watch 时传入 child ctx, 如果此时 parent ctx 被外层 cancel, child ctx 也会被级联 cancel, rch 会被 etcd 关闭,然后 for 循环走到 select 逻辑,此时 child ctx 被取消了,所以 <-ctx.Done() 生效,watch 函数返回

其于 context 可以很好的做到多个 goroutine 协作,超时管理,大大简化了开发工作。这也是 Go 的魅力

原理

type Context interface {
 Deadline() (deadline time.Time, ok bool)
 Done() <-chan struct{}
 Err() error
 Value(key interface{}) interface{}
}

Context 是一个接口

  • Deadline ctx 如果在某个时间点关闭的话,返回该值。否则 ok 为 false
  • Done 返回一个 channel, 如果超时或是取消就会被关闭,实现消息通讯
  • Err 如果当前 ctx 超时或被取消了,那么 Err 返回错误
  • Value 根据某个 key 返回对应的 value, 功能类似字典

目前的实现有 emptyCtx, valueCtx, cancelCtx, timerCtx. 可以基于某个 Parent 派生成 Child Context

func WithValue(parent Context, key, val interface{}) Context
func WithCancel(parent Context) (Context, CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

经过多次派生后,ctx 是一个类似多叉树的结构。当 ctx-1 被 cancel 时,会级联 cancel 以 ctx-1 为根的整棵树,但是原来的 root, ctx2 ctx3 不受影响

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 {
  c.mu.Unlock()
  return // already canceled
 }
 c.err = err
 d, _ := c.done.Load().(chan struct{})
 if d == nil {
  c.done.Store(closedchan)
 } else {
  close(d)
 }
 for child := range c.children {
  // NOTE: acquiring the child's lock while holding parent's lock.
  child.cancel(false, err)
 }
 c.children = nil
 c.mu.Unlock()

 if removeFromParent {
  removeChild(c.Context, c)
 }
}

首先检测 done channel, 如果有人监听,那么 close 掉,这时所有 wait 这个 ctx 的 goroutines 都会收到消息

然后遍历 children map, 依次 cancel 所有 child, 这里类似树的先序遍历。最后 removeFromParent 将自己从父节点中摘除

几个问题

打印 Ctx

WithCancel 为例子,可以看到 child 同时引用了 parent, 而 propagateCancel 函数的存在,parent 也会引用 child(当 parent 是 cancelCtx 类型时)

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
 c := newCancelCtx(parent)
 propagateCancel(parent, &c)
 return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
 return cancelCtx{Context: parent}
}

如果此时打印 ctx, 就会递归调用 String() 方法,就会把 key/value 打印出来。如果此时 value 是非线程安全的,比如 map, 就会引发 concurrent read and write panic

这个案例就是 http 标准库的实现 server.go:2906[2] 行代码,把 http server 保存到 ctx 中

ctx := context.WithValue(baseCtx, ServerContextKey, srv)

最后调用业务层代码时把 ctx 传给了用户

go c.serve(connCtx)

如果此时打印 ctx, 就会打印 http srv 结构体,这里面就有 map. 感兴趣的可以做个实验,拿 ab 压测很容易复现

func stringify(v interface{}) string {
 switch s := v.(type) {
 case stringer:
  return s.String()
 case string:
  return s
 }
 return "<not Stringer>"
}

func (c *valueCtx) String() string {
 return contextName(c.Context) + ".WithValue(type " +
  reflectlite.TypeOf(c.key).String() +
  ", val " + stringify(c.val) + ")"
}

同时注意,后来 go 对此做了部份修复,一定程序上解决了问题。但也记住不要打印 ctx

Key/Value 类型不安全

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
 Context
 key, val interface{}
}

强烈不建义使用 Context 传递过多数据,这里可以看到 key/value 类型都是 interface{}, 编译期无法确定类型,运行期需要断言,有性能和安全问题

关闭底层连接

Context 超时会触发 http pool 关闭掉底层 connection, 导致连接频繁销重建,参考之前的文章[超时控制一个反例]

问题在于,要在哪层处理 tcp 无用数据,如果应用层读完再丢掉,此时连接还是可用的,但是操作系统 tcp stack 处理无用数据,那直接就 close. 而 grpc 就没这个问题,因为多路复用,每个请求都是虚拟的 stream, 如果超时,只需关闭 stream, 无需关闭底层 tcp 连接

双向链表

Context 派生层数比较多时,构成了一个双向链表,key/value 获取很有可能退化成 O(N) 操作,非常慢

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

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}
}

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

每当添加一个 key/value 时都会生成新的 valueCtx, 查询时,如果当前 ctx 不存在 key, 则递归查询 c.Context

提前超时

func test(){
 ctx, cancel := context.WithCancel(ctx)
 defer cancel()

  doSomething(ctx)
}

func doSomething(ctx){
  go doOthers(ctx)
}

当调用栈较深,多人合作时很容易产生这种情况。其实还是没明白 ctx cancel 工作原理,异步 go 出去的业务逻辑需要基于 context.Background() 再派生 child ctx, 否则就会提前超时返回

另外大家容易忽略的点,默认情况下 grpc 会透传超时时间的,比如入口 A 服务调 B, 超时设置了 2s, B 如果用同一个 Context 去调下游 C, 那么超时就要减去 B 自己处理的时间。如果链路比较长,很可能到达 G 服务时就己经超时了

传递超时可以提前释放资源,否则入口超时了,后端还在处理请求

自定义 Ctx

非常不建义自定义 Context, 原因在于源码中处理是不同的

func test(){
 ctx, cancel := context.WithCancel(ctx)
 defer cancel()

  doSomething(ctx)
}

func doSomething(ctx){
  go doOthers(ctx)
}

通过源码可知,parent 引用 child 有两种方式,官方 cancelCtx 类型的是用 map 保存。但是非官方的需要开启 goroutine 去监测。本来业务代码己经 goroutine 满天飞了,不加节制的使用只会增加系统负担

使用建议

最后来总结下 context 使用的几个原则:

  • 除了框架层不要使用 WithValue 携带业务数据,这个类型是 interface{}, 编译期无法确定,运行时assert 有开销。如果真要携带也要用 thread-safe 的数据
  • 一定不要打印 Context, 尤其是从 http 标准库派生出来的,谁知道里面存了什么
  • Context 通常做为第一个参数传给函数,但如果 Context 生命周期等同于结构体,当成结构体成员也可以
  • 尽可能不要自定义用户层 Context, 除非收益巨大
  • 异步 goroutine 逻辑使用 Context 时要清楚谁还持有,会不会提前超时,尤其调 rpc, db, redis 时
  • 派生出来的 child ctx 一定要配合 defer cancel() 使用,释放资源

小结

关于 Context 大家有什么看法,欢迎留言一起讨论,大牛多留言 ^_^

参考资料

[1]Context should go away in GO2: https://faiface.github.io/post/context-should-go-away-go2/,

[2]http server: https://github.com/golang/go/blob/master/src/net/http/server.go#L2878,

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/qmX6nh-XnZIAqVxvbccD9A

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237229次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录