Context上下文简介
当 一个请求被取消或者发生超时时,为了防止资源泄露,这个请求上 的所有goroutine 都应该被退出来。通过context传入,可以 将一些取消信息或者超时信息传递给其他协。
context 相当于 简洁得管理了goroutines的生命周期,context包定义了 不同上下文类型,它跨API边界和进程之间传递截止日期、取消信号和其他请求范围的值。
Context源码解读
1、Context接口
type Context interface {
Deadline() (deadline time.Time, ok bool)
// Deadline()方法 返回当前上下文的截至时间,即该上下文应该被取消的时间
// 如果没有设置超时时间,则返回的ok为false
// Done() 方法方法,当struct通道,有写出时实现
Done() <-chan struct{}
// 当代表此上下文完成的工作应取消时,Done返回一个被关闭的通道,如果无法取消此上下文,则Done可能返回nil
//cancel函数返回后,Done通道的关闭可能会异步进行。
//WithCancel安排在调用cancel时关闭Done;
//WithDeadline安排在期限届满时关闭;
//WithTimeout安排在超时结束时关闭Done。
// Err()方法,代表被关闭的原因
Err() error
// 如果Done尚未关闭,Err返回nil。 如果关闭了Done,Err将返回一个非nil错误,解释原因:如果上下文被取消,则是取消错误;如果上下文的截止日期已过,则超出截止时间错误。
// Value()方法,返回与 key 相关了的值,不存在返回 nil
Value(key interface{}) interface{}
// 仅对传输进程和API边界的请求范围的数据使用上下文值,而不是为了给函数传递可选参数。
// key标识上下文中的特定值,希望在上下文中存储值的函数通常在全局变量中分配一个key,然后将该key用作argument传给context.WithValue and Context.Value
Done()方法 在上下文被取消或超时时会关闭当前监听的通道,表示上下文不再监听,因此关闭可以 作为广播通知,告诉context相关的函数要停止当前工作然后退出。
创建Context时会返回CancelFunc函数,调用CancelFunc函数以及到了超时时间,会关闭Done()方法监听的channel,所以Done()需要放到select中。 父Context取消时,其子Context也会被取消。但是子Context取消时,其父Context不会被取消
Done用于select语句:Stream()使用DoSomething()生成值,并将其发送到输出,直到DoSomething返回错误或ctx.Done关闭
// func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
}
}
Value()方法通过key获取该key在上下文中关联的值,如果该key没有关联的值,则返回nil,多次调用同一个key,返回的结果是一样的
var userKey key
// NewContext返回一个携带值u的新上下文。
func NewContext(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}
// FromContext返回存储在上下文中的用户值(如果有)。
func FromContext(ctx context.Context) (*User, bool) {
u, ok := ctx.Value(userKey).(*User)
return u, ok
}
2、deadlineExceededError 对象
type deadlineExceededError struct{}
// (截止时间超时)对象 Error报错方法:返回字符串
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
// (截止时间超时)对象 Timeout超时方法:返回 是否超时
func (deadlineExceededError) Timeout() bool { return true }
// (截止时间超时)对象 Temporary暂停方法:返回 是否暂停
func (deadlineExceededError) Temporary() bool { return true }
3、 emptyCtx对象
empty是空的Context,但是实现了Context的接口。emptyCtx没有超时时间,不能被取消,也不能存储任何额外信息,所以emptyCtx常用来作为Context的根节点
// 空上下文emptyCtx对象(int类型),以下实现了Context接口
// 没有值没有截至时间。 它不是一个结构体,这个类型的值必须 有不同的地址。
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
}
// 空上下文对象的String方法:如果空上下文对象是background、或者todo。。。,返回不同字符串
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
由于emptyCtx是不能外部访问,所以我们只能用Background和TODO来使用,且Background经常会被使用,经常被当成根节点使用,而TODO在不确定使用什么Context时才会使用。
// 这里定义了 两个 新的空上下文 变量
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Background()返回非nil的空上下文, 它从不取消,没有值,也没有截止日期。
// 它通常由主函数、初始化和测试使用,并作为传入请求的顶级上下文。
func Background() Context {
return background
}
// TODO返回一个非零的空上下文,当不清楚要使用哪个上下文或者它还不可用时 代码应使用context.T0DO(因为周围的函数还没有扩展到接受上下文参数)。
func TODO() Context {
return todo
}
以下使用Background的示例,作为创建出来ctx的父节点
ctx, cancel := context.WithCancel(context.Background())
4、canceler 接口
//定义 取消接口:取消器是可以直接取消的上下文类型。它的实现是:*cancelCtx和*timerCtx。
type canceler interface {
// 带有一个 取消方法:传入 是否远程父级,和错误两个参数
cancel(removeFromParent bool, err error)
// 带有一个 写出通道
Done() <-chan struct{}
}
5、cancelCtx结构体对象
cancelCtx 结构体继承了 Context
// 定义 cancelCtx 取消上下文 的结构体对象
// 一个 canceCtx 对象可以被取消,取消时,它还会取消 实现了canceler接口的所有子级。
type cancelCtx struct {
Context // 包含 Context接口
mu sync.Mutex // 互斥锁字段:用于保护下列字段
done chan struct{} // done字段:结构体类型通道,当调用第一个取消函数时就关闭通道
children map[canceler]struct{} // 子级字段:通过第一次取消调用时,设置该字段为nil
err error // 错误字段:通过第一次取消调用时,设置该字段为 非nil
}
5.1、cancelCtx相关方法
其中cancel()方法+Done()方法, 实现了 canceler 接口, 所以可以被直接canceled
// Value() (传key,返回值)
func (c *cancelCtx) Value(key interface{}) interface{} {
// 1、如果 键key是取消上下文的key,则返回 自身
if key == &cancelCtxKey {
return c
}
// 2、否则,返回 实例的 上下文的key对应的值
return c.Context.Value(key)
}
// Done() :返回的该结构体中的done字段
func (c *cancelCtx) Done() <-chan struct{} {
// 1、互斥锁 ,给结构体实例 本身 加锁
c.mu.Lock()
// 2、如果 实例的写出通道 仍是 nil,则创建一个结构体通道
if c.done == nil {
c.done = make(chan struct{})
}
// 3、将 通道 赋值给d变量
d := c.done
// 4、解锁
c.mu.Unlock()
// 5、返回通道
return d
}
// Err() :返回实例对象的err字段
func (c *cancelCtx) Err() error {
// 1、互斥锁 ,给结构体实例 本身 加锁
c.mu.Lock()
// 2、将 实例本身的 err字段赋值给新变量
err := c.err
// 3、解锁
c.mu.Unlock()
// 4、返回该实例的err字段
return err
}
// 定义 stringer 接口
type stringer interface {
String() string
}
// 上下文命名方法:返回上下文的string值
func contextName(c Context) string {
if s, ok := c.(stringer); ok {
return s.String()
}
return reflectlite.TypeOf(c).String()
}
// String() 取消上下文结构体对象 的 返回字符串 方法:
// 此方法实现了stringer 接口
func (c *cancelCtx) String() string {
return contextName(c.Context) + ".WithCancel"
}
// cancel() 取消上下文的实现逻辑
// 核心是关闭c.done
// 同时会设置c.err = err, c.children = nil
// 依次遍历c.children,每个child分别cancel
// 如果设置了removeFromParent,则将c从其parent的children中删除
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 1、报错时才会调用该函数
if err == nil {
panic("context: internal error: missing cancel error")
}
// 2、实例 上锁
c.mu.Lock()
// 3、如果实例本身的 错误不为空(出现了错误,则证明它已经被取消了,此次调用无意义):解锁,然后直接返回
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
// 4、否则,本身实例 错误是空,将传入的错误 赋值给 实例字段
c.err = err
// 5、 如果当前节点的done为nil,则直接赋值一个关闭的channel。监听一个关闭的channel会直接返回
if c.done == nil {
c.done = closedchan
} else {
// 否则,如果有写出值,则 关闭 该通道
close(c.done)
}
// 6、 轮询 实例的每个子级:并且调用 子级的cancel方法
for child := range c.children {
// NOTE: 在持有父锁的同时获取子锁
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
// 7、如果 是设置了removeFromParent ,则调用移除方法,移除子级 上下文
if removeFromParent {
removeChild(c.Context, c)
}
}
6、timerCtx 结构体对象
// 定义带时间上下文的结构体:带有计时器和截止日期。
// 它通过停止计时器 然后委托给cancelCtx.cancel来实现 取消
type timerCtx struct {
cancelCtx // 嵌入了一个cancelCtx来实现Done和Err。
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
timerCtxt 直接继承了cancelCtx,是在cancelCtx结构体中提供了进一步的扩展,timerCtx可以调用取消函数进行取消,也可以设置超时,当超时时自动取消。
6.1 timerCtx的相关方法
// 1、Deadline(): 获取 截止时间的方法
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
// 2、String():字符串返回 方法: 上下文名字.WithDeadline(截止时间[倒计时])
func (c *timerCtx) String() string {
return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
c.deadline.String() + " [" +
time.Until(c.deadline).String() + "])"
}
// 3、cancel()方法:取消 方法,传入是否来源于父级、错误err
func (c *timerCtx) cancel(removeFromParent bool, err error) {
// 1、 父级直接取消
c.cancelCtx.cancel(false, err)
// 2、如果是子级,则移除子级上下文
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
// 3、停止计时器
c.mu.Lock()
if c.timer != nil {
// 如果timer不为nil,则停止,避免重复执行cancel
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
cancel方法主要调用cancelCtx的cancel进行取消context。
如果timer不为nil,则停止该定时任务,并置为nil。
7、对外 提供Context的主要方法
我们不需要手动实现这个接口,context 包已经给我们提供了两个,一个是 Background(),一个是 TODO(),这两个函数都会返回一个 Context 的实例。只是返回的这两个实例都是空 Context。
7.1 Background()函数可以返回emptyCtx对象,常用来作为根节点
7.2 WithCancel()函数
// WithCancel() 新建一个带有取消方法的上下文 方法:传入上下文,返回上下文(具有新Done通道的父级的副本) 和 一个取消函数
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent) // 初始化一个cancelCtx,并指定父级Context为parent
propagateCancel(parent, &c) // 调用propagateCancel()
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx() 新建一个 取消上下文 对象 的方法:传入上下文,返回一个初始化的cancelCtx结构体对象,结构体Context字段的值是入参Context
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent} // 初始化的上下文是传入的上下文
}
// goroutines统计曾经创建的goroutine的数量;用于测试。
var goroutines int32
// propagateCancel()方法 传播取消方法, 分配在父级为~~时取消子级
func propagateCancel(parent Context, child canceler) {
// 1、如果parent的Done()为nil,说明父级没有cancel,直接返回
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done: // 2、当通道有值时(parent已经被取消),子级(canceler)调用cancel函数
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
// 3、调用 父级取消上下文 函数,如果ok, 则返回下层的 cancelCtx--p
if p, ok := parentCancelCtx(parent); ok {
// 对parent下层的 cancelCtx 上锁
p.mu.Lock()
// 3.1、如果上下文的err有数据,则证明 父级被取消了,此时调用子级取消
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
// 3.2、否则如果 err没有数据(则证明 父级没被取消), 且父级下层的 cancelCtx 没有子级 上下文
if p.children == nil {
//如果p的children未初始化,则进行初始化
p.children = make(map[canceler]struct{})
}
// 然后将 空结构体 赋值给子级map的child key
p.children[child] = struct{}{}
}
// 对parent下层的 cancelCtx 解锁
p.mu.Unlock()
} else {
// 如果调用返回 !ok,(证明通道关闭、取值失败或者不匹配,即parent的Contex实现类不是是cancelCtx)
// 4、新建一个 协程
atomic.AddInt32(&goroutines, +1) // 原子地将delta添加到 &goroutines 并返回新值
go func() {
select {
// 4.1、如果 父级通道关闭,则调用子级 取消方法
case <-parent.Done():
child.cancel(false, parent.Err())
// 子级通道 关闭,则不做处理
case <-child.Done():
}
}()
}
}
WithCancel() 首先调用newCancelCtx初始化一个cancelCtx对象,并指定该对象的Context。
propagateCancel() 的主要功能是如果指定的父节点是cancelCtx类型的,则将当前创建的节点维护到父节点中(通过父节点的children关联)。并且实现当父节点取消时,当前子节点也会被取消。
7.3 通过WithDeadline初始化Context
// 设置截止时间方法:截止日期调整为不晚于d(传入时间),返回父级的副本
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// 1、取出父级的截止时间,如果更早,返回
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
// 2、新建timerCtx实例对象:d为截至时间,上下文用 传入的父级上下文
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 3、调用传播取消 方法
propagateCancel(parent, c)
dur := time.Until(d) // 到d的持续时间
// 4、如果截至时间已过, 新建的上下文对象,调用取消 函数,并传入超过 截止时间 的错误,然后 把c返回
// 当截止日期过期、调用返回的cancel函数或父上下文的Done通道关闭时(以先发生的为准),返回的上下文的Done通道将关闭。
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()
// 5、如果新建的带有截至时间的上下文 的 err为空,则 返回一个截止时间的计时器
// AfterFunc()等待持续时间过去,然后在自己的goroutine中调用f函数,它返回一个计时器,可用于使用Stop方法取消调用
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
WithDeadline方法创建的Context,不仅可以通过手动调用取消函数取消,也可以指定超时的时间点,如果超时则会自动取消。主要通过timerCtx实例对象来 实现以上取消功能。
① 如果创建的子节点的超时时间比父节点的超时时间还晚,则无需使用timerCtx创建,因为父节点取消时,该节点也会创建。所以直接调用WithCancel创建。
②如果晚于父节点,则会创建timerCtx对象,并调用propagateCancel实现与父节点的关联。
③ 如果超时时间已到,则直接调用取消函数。否则开启一个定时器到超时时间点触发取消函数。
7.4 通过WithTimeout初始化Context
// 设置超时函数:调用 设置截至时间的函数(传 此时+超时为 截止时间参数)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
7.5 WithValue 返回Context
通过传入的父级上下文, 和一组k-v,返回一个带有k-v的Context对象–valueCtx
// 求值函数:返回父级的副本,其中与键关联的值是val。
func WithValue(parent Context, key, val interface{}) Context {
// 如果没有键key则报错
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
//定义一个 键值对上下文:它实现该键的值,并将所有其他调用委托给嵌入式上下文
type valueCtx struct {
Context
key, val interface{}
}
Context使用规则
使用 Context 的程序包需要遵循如下的原则来满足接口的一致性以及便于静态分析
不要把 Context 存在一个结构体当中,显式地传入函数。Context 变量需要作为第一个参数使用,一般命名为ctx
即使方法允许,也不要传入一个 nil 的 Context ,如果你不确定你要用什么 Context 的时候传一个 context.TODO
使用 context 的 Value 相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数
同样的 Context 可以用来传递到不同的 goroutine 中,Context 在多个goroutine 中是安全的
Context使用示例
package main
import (
context2 "context"
"fmt"
"time"
)
// 模拟一个最小执行时间的阻塞函数
func inc(a int) int {
res := a + 1 // 虽然我只做了一次简单的 +1 的运算,
time.Sleep(1 * time.Second) // 但是由于我的机器指令集中没有这条指令,
// 所以在我执行了 1000000000 条机器指令, 续了 1s 之后, 我才终于得到结果。B)
return res
}
// 向外部提供的阻塞接口
// 计算 a + b, 注意 a, b 均不能为负
// 传入 a、b 参数
func Add(ctx context2.Context, a, b int) int {
res := 0
// 1、循环a次,res 就执行a次+1
for i := 0; i < a; i++ {
res = inc(res) // res增1,且休眠1s
select {
case <-ctx.Done(): // 当 传入的上下文,关闭通道时(计算被中断),返回-1
return -1
default:
}
}
// 2、循环b次,res 就执行b次+1
for i := 0; i < b; i++ {
res = inc(res)
select {
case <-ctx.Done():
return -1
default:
}
}
return res
}
func main() {
{
// 使用开放的 API 计算 a+b
a := 1
b := 2
// 1、设置超时上下文 为2s
timeout := 2 * time.Second
ctx, _ := context2.WithTimeout(context2.Background(), timeout) // 这里Background() 做了根节点(父级上下文)
// 2、调用 求和函数,且将设置了超时的上下文传入近API边缘
res := Add(ctx, 1, 2)
fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res)
}
{
// 3、手动取消
a := 1
b := 2
ctx, cancel := context2.WithCancel(context2.Background())
go func() {
time.Sleep(2 * time.Second)
cancel() // 在调用处 主动取消
}()
res := Add(ctx, 1, 2)
fmt.Printf("Compute: %d+%d, result: %d\n", a, b, res)
}
}