Go Context
Reference
什么是Context
#godoc
//Package context defines the Context type, which carries deadlines,
//cancellation signals, and other request-scoped values across API boundaries
//and between processes.
Go 1.7 标准库引入 context,中文译作上下文,准确说它是 goroutine 的上下文,定义了,最后期限,取消信号,和其他请求范围内值的API之间的边界和流程。
为什么有Context
Go多用于编写后台服务端代码,Go Server的每一个请求都是通过单独的 Goroutine 处理的,HTTP/RPC 请求的处理器会启动若干新的 Goroutine 访问数据库和其他服务。如下图所示。
每一个 context.Context 都会从最顶层的 Goroutine 一层一层传递到最下层。context.Context 可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。
这些 goroutine 需要共享这个请求的基本数据,例如登陆的 token,处理请求的最大超时时间(如果超过此值再返回数据,请求方因为超时接收不到)等等。当请求被取消或是处理时间太长,这有可能是使用者关闭了浏览器或是已经超过了请求方规定的超时时间,请求方直接放弃了这次请求结果。这时,所有正在为这个请求工作的 goroutine 需要快速退出,因为它们的“工作成果”不再被需要了。在相关联的 goroutine 都退出后,系统就可以回收相关的资源。
再多说一点,Go 语言中的 server 实际上是一个“协程模型”,也就是说一个协程处理一个请求。例如在业务的高峰期,某个下游服务的响应变慢,而当前系统的请求又没有超时控制,或者超时时间设置地过大,那么等待下游服务返回数据的协程就会越来越多。而我们知道,协程是要消耗系统资源的,后果就是协程数激增,内存占用飙涨,甚至导致服务不可用。
其实上述的这种情况通过设置“允许下游最长处理时间”就可以避免。例如,给下游设置的 timeout 是 50 ms,如果超过这个值还没有接收到返回数据,就直接向客户端返回一个默认值或者错误。例如,返回商品的一个默认库存数量。注意,这里设置的超时时间和创建一个 http client 设置的读写超时时间不一样,这里不详细展开。
而 context
包就是为了解决上面所说的这些问题而开发的:在 一组 goroutine 之间传递共享的值、取消信号、deadline……
Context 设计原理
context.Context 是 Go 语言在 1.7 版本中引入标准库的接口,该接口定义了四个需要实现的方法,其中包括:
Deadline() (deadline time.Time, ok bool)
— 返回 context.Context 被取消的时间,也就是完成工作的截止日期和是否设置了超时时间;Done() <-chan struct{}
— 返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;Err() error
— 返回 context.Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值;
1). 如果 context.Context 被取消,会返回 Canceled 错误;
2). 如果 context.Context 超时,会返回 DeadlineExceeded 错误;Value(key interface{}) interface{}
— 从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;
四个方法全都是幂等性的方法
只要是实现了这个4个方法的都属于context
,常见的有gin.Context
// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
每一个 Context 都会从最顶层的 Goroutine 一层一层传递到最下层,这也是 Golang 中上下文最常见的使用方式,如果没有 Context,当上层执行的操作出现错误时,下层其实不会收到错误而是会继续执行下去。
当最上层的 Goroutine 因为某些原因执行失败时,下两层的 Goroutine 由于没有接收到这个信号所以会继续工作;但是当我们正确地使用 Context 时,就可以在下层及时停掉无用的工作减少额外资源的消耗:
这其实就是 Golang 中上下文的最大作用,在不同 Goroutine 之间对信号进行同步避免对计算资源的浪费,与此同时 Context 还能携带以请求为作用域的键值对信息。
eg:
func main() {
ctx,cancel := context.WithTimeout(context.Background(),1 * time.Second)
defer cancel()
go HelloHandle(ctx,500*time.Millisecond)
select {
case <- ctx.Done():
fmt.Println("Hello Handle ",ctx.Err())
}
}
func HelloHandle(ctx context.Context,duration time.Duration) {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
case <-time.After(duration):
fmt.Println("process request with", duration)
}
}
上面的代码,因为过期时间大于处理时间,所以我们有足够的时间处理改请求,所以运行代码如下图所示:
process request with 500ms
Hello Handle context deadline exceeded
HelloHandle函数并没有进入超时的select分支,但是main函数的select却会等待context.Context的超时并打印出Hello Handle context deadline exceeded。如果我们将处理请求的时间增加至2000ms,程序就会因为上下文过期而被终止。
context deadline exceeded
Hello Handle context deadline exceeded
context接口、方法概览
类型 | 名称 | 作用 |
---|---|---|
Context | 接口 | 定义了Context接口的4个方法 |
emptyCtx | int | 实现了Context接口,其实是个空的context |
CancelFunc | 函数 | 取消函数 |
canceler | 接口 | context取消接口,定义了两个方法 |
cancelCtx | 结构体 | 可以被取消 |
timerCtx | 结构体 | 超时会被取消 |
valueCtx | 结构体 | 可以存储k-v对 |
Background | 函数 | 返回一个空的context,常作为根context |
TODO | 函数 | 返回-一个空的context,常用于重构时期,没有合适的context可用 |
WithCancel | 函数 | 基于父context,生成一一个可以取消的context |
newCancelCtx | 函数 | 创建-一个可取消的context |
propagateCancel | 函数 | 向下传递context节点间的取消关系 |
parentCancelCtx | 函数 | 找到第-一个可取消的父节点 |
removeChild | 函数 | 去掉父节点的孩子节点 |
init | 函数 | 包初始化 |
WithDeadline | 函数 | 创建-一个有deadline的context |
WithTimeout | 函数 | 创建-一个有timeout的context |
WithValue | 函数 | 创建一个存储k-v对的context |
使用context的注意事项
// Programs that use Contexts should follow these rules to keep interfaces
// consistent across packages and enable static analysis tools to check context
// propagation:
//
// Do not store Contexts inside a struct type; instead, pass a Context
// explicitly to each function that needs it. The Context should be the first
// parameter, typically named ctx:
1.不要把 Context 存放在结构体中,而应该放在方法中进行传递。上下文应该是首参数,通常命名为ctx:
//
// func DoSomething(ctx context.Context, arg Arg) error {
// // ... use ctx ...
// }
//
// Do not pass a nil Context, even if a function permits it. Pass context.TODO
// if you are unsure about which Context to use.
2.需要传Context的地方,不要传nil,如果知道传什么,可以传递 context.TODO
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
3.Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数;
//
// The same Context may be passed to functions running in different goroutines;
// Contexts are safe for simultaneous use by multiple goroutines.
4.Context是线程安全的
默认上下文
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
context.Background():
返回一个空的context,主要当做根节点的传入context,向下传递.
context.TODO():
返回一个空的context,当某些方法需要传递context,但是此时有没有从上面传递下来的context时,可以使用todo进行代替.
emptyCtx
// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
从上段代码来看可以发现emptyCtx
的指针实现了context.Context
接口的4个方法,返回的都是空值,所以他不会被cancel,没有传递值,没有过期时间。作为Background根context
来派生子context或者作为TODO的临时context
来说都很好理解。但是为什么将emptyCtx
定义为一个int
的类型,而不定义为一个struct
?
通过官方的解释是因为给emptyCtx
赋值的时候必须有不同的地址
我们的默认上下文Background
,TODO
都是new出来的一个emptyCtx
指针,如果emptyCtx
是一个空的struct
那每次 new出来的emptyCtx
指针得内存地址都是相同的,这显然不行。所以才定义为int类型。
取消信号
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {//当父parent为空时则不能创建派生context
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)//将传入的父context包装成私有结构体
propagateCancel(parent, &c)//构建父子上下文之间的关联,当父上下文被取消时,子上下文也会被取消
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
// goroutines counts the number of goroutines ever created; for testing.
var goroutines int32
// propagateCancel arranges for child to be canceled when parent is.
// 向下传递context节点间的取消关系
func propagateCancel(parent Context, child canceler) {
// 如果父节点是个空节点(例如backgroup,todo),无法发出取消信号,所以直接返回
done := parent.Done()
if done == nil {
return // parent is never canceled
}
//当父节点不为空时
select {
case <-done:
// parent is already canceled
//当父节点已经发出取消信号,子节点会立刻取消
child.cancel(false, parent.Err())
return
default:
// 未发出取消信号,continue go
}
//返回最近的可取消的父context
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()//通过互斥锁将cancelCtx对象锁住,下面进行安全的节点关联的操作,将child 节点加入parent的children列表中
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 {
//当父上下文是开发者自定义的类型、实现了 context.Context 接口并在 Done() 方法中返回了非空的管道时;
//运行一个新的 Goroutine 同时监听 parent.Done() 和 child.Done() 两个 Channel;
//在 parent.Done() 关闭时调用 child.cancel 取消子上下文;
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
除了 context.WithCancel
之外,context
包中的另外两个函数 context.WithDeadline
和 context.WithTimeout
也都能创建可以被取消的计时器上下文 context.timerCtx
:
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 parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// 如果当前的deadline时间早于新的deadline,则直接返回派生context,只需保证parent context的过期时间是比child context的时间早就可以,因为parent context同步了取消信号,child context肯定也被取消了
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 {
//通过设置定时器,在指定过期时间调用cancel方法
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
context.WithDeadline
在创建 context.timerCtx
的过程中判断了父上下文的截止日期与当前日期,并通过 time.AfterFunc
创建定时器,当时间超过了截止日期后会调用 context.timerCtx.cancel
同步取消信号。
context.timerCtx
内部不仅通过嵌入 context.cancelCtx
结构体继承了相关的变量和方法,还通过持有的定时器 timer
和截止时间 deadline
实现了定时取消的功能:
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
//停止持有的定时器
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
context.timerCtx.cancel
方法不仅调用了 context.cancelCtx.cancel
,还会停止持有的定时器减少不必要的资源浪费。
传值方法
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
context.valueCtx
结构体会将除了 Value
之外的 Err、Deadline
等方法代理到父上下文中,它只会响应 context.valueCtx.Value
方法,该方法的实现也很简单:
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)
}
如果 context.valueCtx
中存储的键值对与 context.valueCtx.Value
方法中传入的参数不匹配,就会从父上下文中查找该键对应的值直到某个父上下文中返回 nil
或者查找到对应的值。
参考资料
context 官博: https://blog.golang.org/context.
Go 语言设计与实现: https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/.
深度解密Go语言之context: https://zhuanlan.zhihu.com/p/68792989.