深入理解 Go Context

什么是 Context

Context 的最常见但也是最不准确的翻译是 ‘上下文’(因为程序里通常只需要上文),其实译为 ‘语境’ 更为合适,意思是当前说话的环境。最直观的作用是提供一些必要的信息:

...

唐僧:“悟空~”

question:唐僧的“悟空” 表达了怎样的心理?

answer:。。。去你的

Context 的概念本身比较宽泛,从系统角度说,线程/进程 的切换时,需要先保存当前寄存器和栈指针,然后载入下一个 进程/线程 需要的寄存器和栈。寄存器和栈就是进程/线程的 Context。

在不同编程语言中,也有不同体现:

如 c 语言的 errno (摘自某呼):

注意过 errno 这个全局变量的朋友会发现,这个全局变量其实有可能不是一个真正的变量.它返回了一个本地线程的存储空间.它实际上是每个线程有一份.这里,其实 C 语言运行时已经悄悄变成了多份,而对应当前线程的实例用本地线程保存,它就是一个 context

又例如 Javascript 在浏览器中运行就有浏览器作为环境提供 window 对象,而在 node.js 环境下面运行就没有 window 对象。

照此看来,Context 好像就是一个 ‘全局变量’,那为什么不直接声明全局变量,非要用 Context 这个生涩的概念呢?

在软件工程中,对全局变量基本持否定态度,一是是代码变得耦合,二是暴露了多余的信息,三是全局变量在多线程环境下使用锁浪费 CPU 资源。不过它有很好的效果:间接的提升了某些变量的作用域,保证了这些数据的生命周期

于是出现了 不那么全局的全局变量 ,例如 线程局部 的全局变量(可以做到线程安全)或者 包局部 的全局变量。很多语言的 this ,其实也是如此。

另外还有匿名形式的 闭包 局部的全局变量

再结合轮子哥说的:

每一段程序都有很多外部变量。只有像 Add 这种简单的函数才是没有外部变量的。一旦你的一段程序有了外部变量,这段程序就不完整,不能独立运行。你为了使他们运行,就要给所有的外部变量一个一个写一些值进去。这些值的集合就叫 Context

那么我们可以认为,Context 就是把一些信息打包聚合到一起,形成一个模块交互的语境,各个模块像传递包裹一样取用它,而不是通过全局变量来访问它。

Go 语言里的 Context

Context 的使用

Go 语言的 Context 在携带信息的基础上,增加了非常实用的功能,设计也非常简洁巧妙。标准库提供了可携带 value 的 Context、可取消的 Context 和 可超时的 Context 。

携带 value 的 Context

前面提到 Context 最基本的作用是携带语境中的一些信息,比如一些参数。但是问题来了,所有参数都要放到 Context 吗?哪些应该、哪些不应该?如果一个函数如下:

func a(key string, value interface, id int){

...

}

如果把参数全都放到 context:

func a(ctx context.Context){

...

}

前者我们可以一目了然的从函数签名中获取或猜出一些关于这个函数的大概信息,而后者只看函数签名获得不了什么信息,需要仔细的从代码里读。很明显,前者可读性更高。一个良好的 API 设计,应该从函数签名就清晰的理解函数的逻辑。

使用 Context 携带参数会让接口定义更加模糊。那么什么样的信息应该放到 Context 里呢?官方注释如下:

Use context values only for request-scoped data that transits processes and API boundaries, not for passing optional parameters to functions.

也就是说,应该保存 Request 范畴的值:

任何关于 Context 自身的都是 Request 范畴的(这俩同生共死)

从 Request 数据衍生出来,并且随着 Request 的结束而终结

。。。好像这句话说了和没说差不多?在处理请求的时候,难道不是所有的信息都来自 Request ?

其实通常来说, Context.Value 应该是 告知性质 的东西,而不是 控制性质 的东西。

哪些不是控制性质的?

Request ID

只是给每个 RPC 调用一个 ID,而没有实际意义

这就是个数字/字符串,反正你也不会用其作为逻辑判断

一般也就是日志的时候需要记录一下

而 logger 本身不是 Request 范畴,所以 logger 不应该在 Context 里

非 Request 范畴的 logger 应该只是利用 Context 信息来修饰日志

User ID ,比如可以在 jwt 中间件解析出 userID 然后带在 Context 里再传给 controller。

Incoming Request ID

显然是控制性质的:

数据库连接

显然会非常严重的影响逻辑

因此这应该在函数参数里,明确表示出来

...

关于 可携带 value 的 Context,还有一个值得注意的地方是:Context 本身是不可变的(immutable),让一个 Context 携带新的参数并不是一个 “setter” 来修改 Context 值,而是通过“包含”的形式,生成一个新的 Context 包含原有 Context,形成链式结构。在下面实现的时候继续讨论。

可取消 和 可超时的 Context

为什么要取消(超时的本质也是取消,只不过通过计时器触发取消操作)?

这和 Go 语言的 goroutine 有关。当你在 c 程序中 fork 一个新的进程,你会得到一个 PID,你可以通过这个 PID 向它发送信号来停止它的运行。

可是当你启动一个 goroutine 时,你并不会得到一个这个‘线程‘的 ID,那么要如何才能关掉它呢?答案就是 可取消的 Context。

官方示例:

package main

import (

"context"

"fmt"

"time"

)

func main() {

d := time.Now().Add(50 * time.Millisecond)

ctx, cancel := context.WithDeadline(context.Background(), d)

// Even though ctx will be expired, it is good practice to call its

// cancelation function in any case. Failure to do so may keep the

// context and its parent alive longer than necessary.

defer cancel()

select {

case

fmt.Println("overslept")

case

fmt.Println(ctx.Err())

}

}

几点问题

当你搜索关于 Go Context 的博客的时候,通常你会看到一些规则:

不要将 Context 放入结构体, Context 应该作为第一个参数传入,命名为 ctx.

即使函数允许, 也不要传入nil 的 Context. 如果不知道用哪种 Context,可以使用 context.TODO().

使用 context 的 Value 相关方法,只应该用于在程序和接口中传递和请求相关数据,不能用它来传递一些可选的参数

相同的 Context 可以传递给在不同的 goroutine; Context 是并发安全的.

可是有几点问题:

为什么不应该放在结构体?

最开始已经说明了,Context 最基本的作用,是对一些 不那么全局的全局变量 的打包,把它放到结构体,其生存周期和作用域是无法控制的,相当于把它变成了它所在包的一个全局变量。理想情况下,Context 存在于调用栈(Call Stack) 中,所以通过参数传递。

为什么 HTTP 包的 Request 结构体持有 context?

Request 本身就是一堆参数的集合,只不过参数太多单独写成结构体了而已,这堆参数在请求结束时或者读写超时时(conn readTimeout/writeTimeout)就应该释放,需要一个可超时的 Context 来协助。那为什么不把请求参数都放在 Context 呢,这个问题前面已经讨论过了,可读性是非常重要的。

为什么是并发安全的?

Context 本身的实现是不可变的(immutable),既然不可变,那当然是线程安全的。并且通过 Context.Done() 返回的通道可以协调 goroutine 的行为。

Go 的 Context 实现

在标准库里,Context 是一个接口:

type Context interface {

Deadline() (deadline time.Time, ok bool)

Done()

Err() error

Value(key interface{}) interface{}

}

而我们常用的 context.Background() 返回的是一个最基本的全局 context:background,是一个什么功能也没有的 emptyCtx:

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {

return

}

func (*emptyCtx) Done()

return nil

}

func (*emptyCtx) Err() error {

return nil

}

func (*emptyCtx) Value(key interface{}) interface{} {

return nil

}

var (

background = new(emptyCtx)

todo = new(emptyCtx)

)

其他所有的 Context 都应该衍生自这两个基本的 ctx,生成新的 context 的方式是找一个 ‘父亲’ ,然后复制它,再结合 value 或者 timer 生成新的 context。

withValue

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} // 返回的是一个指针

}

type valueCtx struct {

Context // 注意这里使用匿名域

key, val interface{}

}

每次添加 value 不是改变了context ,而是在原有的 context 基础上重新生成一个,形成了一条链。获取 value 的时候是逆序的:

func (c *valueCtx) Value(key interface{}) interface{} {

if c.key == key {

return c.val

}

return c.Context.Value(key)

}

先看最后一个节点的键值对,如果不是,那么沿着链往上查找:

value_ctx_chain.png

withCancel

由于可超时的 Context 是基于可取消的 Context 实现的,所以这里只讨论 cancelCtx:

type canceler interface {

cancel(removeFromParent bool, err error)

Done()

}

type cancelCtx struct {

Context

mu sync.Mutex // 由于多个线程都可能执行 ctx.Cancel(),要加锁

done chan struct{} // created lazily, closed by first cancel call

children map[canceler]struct{} // 由于需要在父节点取消时取消其所有字节点,所以记录其所有可取消子节点

err error // set to non-nil by the first cancel call

}

func (c *cancelCtx) Done()

c.mu.Lock()

if c.done == nil {

c.done = make(chan struct{})

}

d := c.done

c.mu.Unlock()

return d

}

生成一个新的 可取消 Context 的时候,需要传入一个父 Context 节点,并且通过父节点找到祖先节点里面最近的一个可取消的 Context 节点,然后把自己记录在那个祖先节点的 children 里面,这样在祖先被 cancel 的时候,新的这个 Context 也会被取消。不过为什么是祖先节点而不是父节点呢?因为可能有如下情况(图中箭头方向代表生长方向):

cancelCtx.png

其父节点可能不是可取消的,所以没法记录 children,所以不难理解代码了:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {

c := newCancelCtx(parent) // 生成一个新的可取消节点

propagateCancel(parent, &c) // 找到可取消祖先并记录自己到祖先的 children

return &c, func() { c.cancel(true, Canceled) }

}

// newCancelCtx returns an initialized cancelCtx.

func newCancelCtx(parent Context) cancelCtx {

return cancelCtx{Context: parent}

}

// propagateCancel arranges for child to be canceled when parent is.

func propagateCancel(parent Context, child canceler) {

if parent.Done() == nil {

return // 这里尤其注意,parent.Done() 返回 nil,表示整个链上都没有可取消/可超时的 context。因为新的 Context 在包含父节点的时候,都是采用匿名字段,也就是说,如果新的 Context 本身没有某个函数,但是它的匿名字段上有那个函数,那么该函数是可以直接被新的 Context 调用的。如此就可以一直追溯到 background 节点,而正好这个根节点是有 Done() 这个函数,并且返回 nil。另外,不可能出现中间一个可取消 context 调用 Done() 返回 nil,看实现便知。

}

if p, ok := parentCancelCtx(parent); ok {

p.mu.Lock()

if p.err != nil {

// parent has already been canceled

child.cancel(false, p.err)

} else {

if p.children == nil {

p.children = make(map[canceler]struct{})

}

p.children[child] = struct{}{}

}

p.mu.Unlock()

} else { // 没想通的是这里,什么情况会走到这步呢?

go func() {

select {

case

child.cancel(false, parent.Err())

case

}

}()

}

}

func parentCancelCtx(parent Context) (*cancelCtx, bool) {

for { // 沿着父节点往上找,直到找到一个 可取消的/可超时的 祖先节点

switch c := parent.(type) {

case *cancelCtx:

return c, true

case *timerCtx:

return &c.cancelCtx, true

case *valueCtx:

parent = c.Context

default:

return nil, false

}

}

}

知道如何注册 cancelCtx,那么具体 cancel 的实现也很简单了,就是先取消自己,然后根据 children 递归遍历并取消所有可取消子节点。代码就不贴了,有兴趣自己看一遍完整源码比较合适。

最后再放一张图,更清楚的理解它们的关系:

ctx_relation.png

reference

context c语言作用,理解 Go context相关推荐

  1. hadoop开发必读:认识Context类的作用

    问题导读: 1.Context能干什么? 2.你对Context类了解多少? 3.Context在mapreduce中的作用是什么? 本文实在能够阅读源码的基础上一个继续,如果你还不能阅读源码,请参考 ...

  2. Hadoop中Context类的作用和Mapper<LongWritable, Text, Text, LongWritable>.Context context是怎么回事【笔记自用】

    问题导读: 1.Context能干什么? 2.你对Context类了解多少? 3.Context在mapreduce中的作用是什么? 下面我们通过来源码,来得到Context的作用: 下面主要对Set ...

  3. 理解GO CONTEXT机制

    1 什么是Context 最近在公司分析gRPC源码,proto文件生成的代码,接口函数第一个参数统一是ctx context.Context接口,公司不少同事都不了解这样设计的出发点是什么,其实我也 ...

  4. Hadoop中Context类的作用

    问题导读: 1.Context能干什么? 2.你对Context类了解多少? 3.Context在mapreduce中的作用是什么? 下面我们通过来源码,来得到Context的作用: 下面主要对Set ...

  5. Spring Data JPA 之 理解 Persistence Context 的核心概念

    21 理解 Persistence Context 的核心概念 21.1 Persistence Context 相关核心概念 21.1.1 EntityManagerFactory 和 Persis ...

  6. react源码分析:深度理解React.Context

    开篇 在 React 中提供了一种「数据管理」机制:React.context,大家可能对它比较陌生,日常开发直接使用它的场景也并不多. 但提起 react-redux 通过 Provider 将 s ...

  7. C语言里的和*的简单作用理解

    ##C语言里的&和*的简单作用理解   自己在C里,关于&与*的作用老是迷糊了好久,学了也是忘记,所以在此再做笔记,以便给有同样困扰的小白一起学习. 首先我们要知道,一个变量存在计算机 ...

  8. 与context的关系_Go中的Context超时和关闭是如何实现的呢?

    Go语言中文网,致力于每日分享编码知识,欢迎关注我,会有意想不到的收获! 01 前言 Golang的context的作用就不多说了,就是用来管理调用上下文的,控制一个请求的生命周期.golang的co ...

  9. context:annotation-config/,mvc:annotation-driven/和context:component-scan之间的关系

    现在常用框架中SpringMVC.xml配置是: <mvc:annotation-driven/>和<context:component-scan> 那么<context ...

最新文章

  1. Wiki为什么会流行
  2. AWS ELB Sticky Session有问题?别忘了AWSELB cookie
  3. C++知识点51——虚函数与纯虚函数(下)
  4. 开发部署提速8倍!这款IDE插件了解一下?
  5. android view 源码分析,Android ViewPager源码详细分析
  6. DCMTK:将hardcopy硬拷贝特征曲线文件转换为softcopy软拷贝格式
  7. 荣耀系统更新服务器不可用,荣耀确认系统更新方式 4月1日前发布的机型固件升级由华为负责...
  8. 互联网日报 | 7月15日 星期四 | B站赠送所有用户1天大会员;饿了么投入3亿用于今夏骑手保障;小米智能工厂二期开工...
  9. 动画函数,为任意一个元素移动到指定的目标位置
  10. matlab通用程序,三次样条差值-matlab通用程序
  11. Web 前端开发框架收集
  12. 在Python中如何优雅地处理PDF文件
  13. cad计算机配置要求,CAD对电脑配置有什么要求?CAD对电脑配置有什么要求?
  14. 【深度学习环境配置二】【Pytorch安装详解-内附下载链接】基于win 10+TITAN XP+CUDA11.1+python3.7+vs2019的pytorch安装
  15. Android 拍摄(横 \ 竖屏)视频的懒人之路
  16. 域名查询工具DMitry
  17. java 微信文章评论点赞_使用fiddler抓取微信公众号文章的阅读数、点赞数、评论数...
  18. 时钟源系统(NTP时间同步服务器)应用农产品追溯系统
  19. 离婚时,住房公积金分割吗?
  20. java怎么把背景设成纯透明,怎么把BufferedImage设置背景为透明

热门文章

  1. Rails I18n验证弃用警告
  2. 将参数传递给Bash函数
  3. 获取列表的最后一个元素
  4. java 协议开发_用Java的NIO开发网络协议
  5. PTA—考试座位号(C语言)
  6. c++之std::distance()函数
  7. Qt窗口部件——QFrame/QAbstractButton/QLineEdit/QAbstractSpinBox/QAbstractSlider
  8. oraccle 索引管理
  9. Unity ToLua 中Update的调用流程
  10. 新加坡推出人工智能计划AI.SG 迎战人工智能和数据科学关键难题