Golang context 实现原理与源码分析

0 context入门介绍

context是Golang应用开发常用的并发控制技术,主要在异步场景中用于实现并发协调以及对 goroutine 的生命周期控制,它与WaitGroup最大的不同点是context对于派生goroutine有更强的控制力,它可以控制多级的goroutine。

context实际上只定义了接口,凡是实现该接口的类都可称为是一种context,官方包中实现了几个常用的context,分别可用于不同的场景;

  • cancelCtx实现了Context接口,通过WithCancel()WithCancelCause()创建cancelCtx实例;
  • timerCtx实现了Context接口,通过WithDeadline()WithTimeout()创建timerCtx实例;
  • valueCtx实现了Context接口,通过WithValue()创建valueCtx实例;

context译作"上下文",准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。它可以控制一组呈树状结构的goroutine,每个goroutine拥有相同的上下文。以上三种context实例可互为父节点,父goroutine派生出子goroutine,而子goroutine又继续派生新的goroutine,即多级goroutine。 在Go 里,我们不能直接杀死协程,协程的关闭一般会用 channel+select 方式来控制。但是在多级goroutine的情况下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline 等,而且可以同时被关闭。再用 channel+select 就会比较麻烦,这时就可以通过 context 来实现。

1. Context接口

1.1 context.Context接口定义

go 复制代码
type Context interface {
    Deadline() (deadline time.Time, ok bool)//返回 context 的过期时间;
    Done() <-chan struct{}					//返回 context 中的 channel;
    Err() error								//返回错误;
    Value(key interface{}) interface{}		//返回 context 中的对应 key 的值.
}
  1. Deadline():该方法返回一个deadline和标识是否已设置deadline的bool值,如果没有设置deadline,则ok == false,此时deadline为一个初始值的time.Time值
  2. Done():该方法返回一个channel,需要在select-case语句中使用,如"case <-context.Done():"。
    • 当context关闭后,Done()返回一个被关闭的管道,关闭的管道仍然是可读的,据此goroutine可以收到关闭请求;
    • 当context还未关闭时,Done()返回nil。
  3. Err():该方法描述context关闭的原因。关闭原因由context实现控制,不需要用户设置。比如Deadline context,关闭原因可能是因为deadline,也可能提前被主动关闭,那么关闭原因就会不同:
    • 当context关闭后,Err()返回context的关闭原因;
      • deadline超时关闭:"context deadline exceeded";
      • cancel主动关闭: "context canceled"。
    • 当context还未关闭时,Err()返回nil;
  4. Value():有一种context即valueCtx,它不是用于控制呈树状分布的goroutine,而是用于在树状分布的goroutine间传递信息。Value()方法就是用于此种类型的context,该方法根据key值查询map中的value。

2. emptyCtx

context包中定义了一个空的context, 名为emptyCtx,用于context的根节点,空的context只是简单的实现了Context,本身不包含任何值,仅用于其他context的父节点。

2.1 emptyCtx源码

go 复制代码
type emptyCtx int	//emptyCtx 是一个空的 context,本质上类型为一个整型;

//Deadline 方法会返回一个公元元年时间以及 false 的 flag,标识当前 context 不存在过期时间;
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {return}

//Done 方法返回一个 nil 值,用户无论往 nil 中写入或者读取数据,均会陷入阻塞;
func (*emptyCtx) Done() <-chan struct{} {return nil}

func (*emptyCtx) Err() error {return nil}//Err 方法返回的错误永远为 nil;

func (*emptyCtx) Value(key interface{}) interface{} {return nil}//Value 方法返回的 value 同样永远为 nil.

2.2 context.Background() & context.TODO()

emptyCtx通过下面两个导出的函数(首字母大写)对外公开:我们所常用的 context.Background()context.TODO() 方法。

context包提供了4个方法创建不同类型的context,使用这四个方法时如果没有父context,都需要传入emptyCtx ,即backgroud或todo作为其父节点:

go 复制代码
var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)
//context.Background()函数返回一个空的上下文对象,被视为所有上下文树的根节点,不需要传递值或取消信号。
func Background() Context {return background}
//context.TODO()函数返回一个空的上下文对象,用于该部分代码还未确定具体需要哪种上下文对象,
func TODO() Context {return todo}

3. cancelCtx

3.1 canceler接口定义

go 复制代码
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

实现了上面定义的两个方法的 Context,就表明该 Context 是可取消的。源码中有两个类型实现了 canceler 接口:*cancelCtx 和 *timerCtx。注意是加了 * 号的,是这两个结构体内部的指针实现了 canceler 接口。

3.2 cancelCtx 数据结构

go 复制代码
type cancelCtx struct {
    Context							//嵌入式接口类型,cancelCtx 必然为某个context的子context;
   
    mu       sync.Mutex            // 互斥锁,保护以下字段不受并发访问的影响
    done     atomic.Value          // 原子通道,第一次调用取消函数时被惰性创建,在该context及其后代context都被取消时关闭
    children map[canceler]struct{} // 值为struct{}其实是一个set,保存当前上下文的所有子上下文,第一次取消调用时设为nil
    err      error                 // 第一次取消操作时设置为一个错误值,对此上下文及后代上下文进行取消操作返回该错误
}

这是一个可以取消的 Context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 Context。

3.3 Done 方法

go 复制代码
func (c *cancelCtx) Done() <-chan struct{} {
    d := c.done.Load()				//基于 atomic 包,读取 cancelCtx 中的 chan;倘若已存在,则直接返回;
    if d != nil {
        return d.(chan struct{})
    }
    c.mu.Lock()						// 加锁后,再次检查 chan 是否存在,若存在则返回;(double check)
    defer c.mu.Unlock()
    d = c.done.Load()
    if d == nil {
        d = make(chan struct{})
        c.done.Store(d)				//初始化 chan 存储到 aotmic.Value 当中,并返回.(懒加载机制)
    }
    return d.(chan struct{})
}

c.done 使用了惰性加载(lazy loading)的机制,只有一次调用 Done() 方法的时候才会被创建,且通过加锁二次检查,确保在多个goroutine同时调用 Done() 方法时,只有第一个goroutine创建通道,其他goroutine均复用已创建的通道。函数返回的是一个只读的 channel,一般通过搭配 select 来使用,当channel关闭后,就会立即读出零值,据此可以判断cancelCtx是否被取消。

3.4 Errl() 方法

go 复制代码
func (c *cancelCtx) Err() error {
    c.mu.Lock()
    err := c.err
    c.mu.Unlock()
    return err
}

cancelCtx.err默认是nil,在context被cancel时指定一个error变量: "context canceled"。

3.5 Valuel() 方法

go 复制代码
func (c *cancelCtx) Value(key any) any {
    if key == &cancelCtxKey {		//倘若 key 特定值 &cancelCtxKey,则返回 cancelCtx 自身的指针;
        return c
    }
    return value(c.Context, key)	//否则遵循 valueCtx 的思路取值返回
}

倘若 key 特定值 &cancelCtxKey,则返回 cancelCtx 自身的指针(基于 cancelCtxKey 为 key 取值时返回 cancelCtx 自身,是 cancelCtx 特有的协议)

3.6 context.WithCancel()方法

context.WithCancel()方法 是Go语言中的context包提供的函数之一,用于创建一个可取消的上下文对象。

go 复制代码
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    if parent == nil {			//校验参数parent------父context 非空;
        panic("cannot create context from nil parent")
    }
    c := newCancelCtx(parent)	//注入parent------父context构造cancelCtx;
    propagateCancel(parent, &c)	//propagateCancel方法内启动一个守护协程,当父context终止时,该cancelCtx 也被终止;
    
    return &c, func(){ c.cancel(true, Canceled)}//将构造的cancelCtx返回,同时返回终止该cancelCtx的闭包函数cancel 
    //第一个参数是 true,也就是说取消的时候,需要将自己从父节点里删除。第二个参数则是一个固定的取消错误类型:
}

context.WithCancel()函数接受一个父上下文对象parent 作为参数,返回一个新的上下文cancelCtx对象ctx 及其对应的取消函数cancel 。当调用取消函数时,该上下文对象及其所有后代上下文对象均会被取消。

3.6.1 newCancelCtx方法

go 复制代码
func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{Context: parent}
}

注入parent 父 context 后,返回一个新构造的的 cancelCtx.

3.6.2 propagateCancel方法

go 复制代码
func propagateCancel(parent Context, child canceler) {//parent即父协程,child即当前协程
    done := parent.Done()
    if done == nil {
        return // parent 是不会被 cancel 的类型(如 emptyCtx),则直接返回
    }

    select {
    case <-done:
        //  parent 已经被 cancel,则直接终止子 context,并以 parent 的 err 作为子 context 的 err
        child.cancel(false, parent.Err())
        return
    default:
    }
	//parentCancelCtx通过 parent.Value(&cancelCtxKey)判断是否是cancelCtx 类型
	//倘若以特定的 cancelCtxKey 从 parent 中通过parent.Value()取值,取得的 value 是 parent 本身,则返回 true. 
	///倘若 parent 的 channel 已关闭或者是不会被 cancel 的类型,则返回 false;
    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 {
        atomic.AddInt32(&goroutines, +1)
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done()://用于退出go协程
            }
        }()
    }
}

propagateCancel 方法顾名思义,用以传递父子 context 之间的 cancel 事件,向上寻找可以"挂靠"的"可取消"的 context,并且"挂靠"上去。这样,调用上层 cancel 方法的时候,就可以层层传递,将那些挂靠的子 context 同时"取消"。

当通过WithCancel(parent Context)创建一个新的cancelContext时propagateCancel 被调用,用来确保当parent父context终止时,该cancelCtx 也被终止:

  1. 若 parent 是不会被 cancel 的类型(如 emptyCtx),不需要传递,则直接返回;
  2. 若 parent 已经被 cancel,则直接终止子 context,并以 parent 的 err 作为子 context 的 err;
  3. 若 parent 是 cancelCtx 的类型,则加锁,并将子 context 添加到 parent 的 children map 当中;
  4. 若 parent 不是 cancelCtx 类型,但又存在 cancel 的能力(比如用户自定义实现的 context),则启动一个协程,通过多路复用的方式监控 parent 状态,倘若其终止,则同时终止子 context,并传递 parent 的 err

3.6.3 cancelCtx.cancel()

go 复制代码
//第一个 removeFromParent 是一个 bool 值,表示当前 context 是否需要从父 context 的 children set 中删除;
//若当前的cancel是由父节点取消引起的,由于父节点已取消,则removeFromParent可以为false
//第二个 err 则是 cancel 后需要展示的错误;
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
	//校验传入的 err 是否为空,若为空则 panic
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    //加锁;
    c.mu.Lock()
    //校验 cancelCtx 自带的 err 是否已经非空,若非空说明已被 cancel,则解锁返回
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    //将传入的 err 赋给 cancelCtx.err
    c.err = err
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
    	// //若 channel 此前未初始化,则直接注入一个 closedChan,否则关闭该 channel;
        c.done.Store(closedchan)
    } else {
        close(d)
    }
    //遍历当前 cancelCtx 的 children set,依次将 children context 都进行 cancel;
    for child := range c.children {
        // 且此时child.cancel第一个参数为false,因为父context已经关闭,会将child从set中删除
        // 即若当前的cancel是由父节点取消引起的,则removeFromParent 则可以为false
        child.cancel(false, err)
    }
    c.children = nil
    //解锁.
    c.mu.Unlock()
	// 根据传入的 removeFromParent flag 判断是否需要手动把 cancelCtx 从 parent 的 children set 中移除.
    if removeFromParent {
    	//如果 parent 不是 cancelCtx,直接返回(因为只有 cancelCtx 才有 children set) 
		//加锁;从 parent 的 children set 中delete删除对应 child解锁返回.
        removeChild(c.Context, c)
    }
}

context.WithCancel() 函数返回一个新的上下文(context)以及一个可用于取消该上下文的取消函数c.cancel(true, Canceled)。当调用cancel()取消函数时,将通过 context 的取消信号来通知这个上下文相关联的所有操作停止执行并释放资源。步骤如下:

  1. 判断err参数是否为空,若为空则引发panic("context: internal error: missing cancel error"),否则首先加锁
  2. 判断cancelCtx 自带的 err 是否已经非空,若非空说明cancelCtx 已被 cancel,则解锁返回
  3. 判断channel是否初始化,若未初始化,则直接注入一个 closedChan关闭的通道,否则关闭已有的channel
  4. 将所有的子contex一起取消,并解锁
  5. 加锁,从 parent 的 children set 中delete删除所有 child,解锁返回.

3.7 context.WithCancelCause()方法

context.WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) 函数是 Go 1.20 版本才新增的,其功能类似于 context.WithCancel(),但是它可以设置额外的取消原因,也就是 error 信息,返回的 cancel 函数被调用时,需传入一个 error 参数。

go 复制代码
ctx, cancelFunc := context.WithCancelCause(parentCtx)
defer cancelFunc(errors.New("原因"))

4. timerCtx

4.1 timerCtx结构

go 复制代码
type timerCtx struct {
    cancelCtx     // 嵌入式结构体,继承了 cancelCtx 中的所有字段和方法
    
    timer    *time.Timer // 当前 context 所关联的 Timer,在 cancelCtx.mu时间
	deadline time.Time	//预期的上下文超时时间
}

timerCtx 在 cancelCtx 基础上又做了一层封装,除了继承 cancelCtx 的能力之外,新增了一个 time.Timer 用于定时终止 context;另外新增了一个 deadline 字段用于字段 timerCtx 的过期时间.

4.2 timerCtx.Deadline()方法

go 复制代码
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}

由于继承了 cancelCtx 结构体,timerCtx 可以从其父亲结构体获得取消能力,同时也可以使用它的成员变量 timer 来设置超时。当计时器触发时,会使用与该上下文相关联的取消函数来取消该上下文中运行的所有操作。

4.3 context.WithTimeout()方法

go 复制代码
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

context.WithTimeout 方法用于构造一个 timerCtx,本质上会调用 context.WithDeadline 方法,截止时间是time.Now().Add(timeout):

4.4 context.WithDeadline()方法

go 复制代码
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	//校验 parent context 非空;
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    //校验 parent是否可过期, 且过期时间是否早于自己,若是,则构造一个 cancelCtx 返回即可;
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    //构造出一个新的 timerCtx结构体;
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    //调用propagateCancel启动守护方法,同步 parent 的 cancel 事件到子 context;
    propagateCancel(parent, c)
    
    dur := time.Until(d)
    //判断过期时间是否已到,若是,直接 cancel timerCtx,并返回 DeadlineExceeded 的错误;
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(false, Canceled) }
    }
    
    //加锁; c.cancel() 和 time.AfterFunc() 两个操作的原子性和同步性确保在计时器触发前或在取消方法执行后不再触发计时器。
    c.mu.Lock()
    defer c.mu.Unlock()
    //c.err为空,即没有终止
    if c.err == nil {
    	//启动time.Timer,达到过期时间后会调用cancel终止该 timerCtx,并返回 DeadlineExceeded 的错误;
    	//time.AfterFunc(dur, func()) 函数的工作方式是异步的,它创建一个新的 Goroutine 来运行计时器并等待计时器触发。
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    // 返回 timerCtx,以及一个封装了 cancel 逻辑的闭包 cancel 函数.
    return c, func() { c.cancel(true, Canceled) }
}

WithDeadline 函数会创建一个新的到期时间戳的上下文。如果在到期时间之前完成操作,则操作正常完成;否则,如果到期时间已经过了,上下文将被标记为超时,并将其传播给与此上下文相关联的所有操作,从而达到错误处理和资源释放的目的。返回值是一个设置好到期时间戳的子上下文对象,以及一个取消函数(CancelFunc),可以随时用于删除此上下文及其所有子级上下文。创建过程如下:

  1. 判断parent是否为空,若为空,则引发panic("cannot create context from nil parent")
  2. 判断parent是否可过期, 且过期时间是否早于自己,若是,则构造一个 cancelCtx 返回即可;
  3. 构造出一个新的 timerCtx结构体,并调用propagateCancel启动守护方法,同步 parent 的 cancel 事件到子 context;
  4. 判断过期时间是否已到,若是,直接 cancel timerCtx,并返回 DeadlineExceeded 的错误;
  5. 加锁;并启动time.Timer,这会开启一个新的协程,当Timer达到过期时间后会调用cancel终止该 timerCtx,并返回 DeadlineExceeded 的err;
  6. 返回 timerCtx,以及一个封装了 cancel 逻辑的闭包 cancel 函数.

4.5 timerCtx.cancel()方法

go 复制代码
func (c *timerCtx) cancel(removeFromParent bool, err error) {
	//复用继承的 cancelCtx 的 cancel 能力,进行 cancel 处理;
    c.cancelCtx.cancel(false, err)
    //判断是否需要手动从 parent 的 children set 中移除,若是则进行处理
    if removeFromParent {
        removeChild(c.cancelCtx.Context, c)
    }
    // 加锁;
    c.mu.Lock()
    if c.timer != nil {
    	//停止 time.Timer
        c.timer.Stop()
        c.timer = nil
    }
    //解锁返回.
    c.mu.Unlock()
}

由于继承了 cancelCtx 结构体,timerCtx 可以从其父亲结构体获得取消能力,调用cancel()时过程如下

  1. 首先调用父亲结构体的cancel()函数,不过此时父亲结构体的cancel()第一个参数removeFromParent 为false,因为不需要删除父亲结点的所有子结点
  2. 判断是否需要将该timerCtx手动从 parent 的 children set 中移除,若是则进行处理
  3. 之后加锁,暂停timer计数器并置空,之后解锁返回

5 valueCtx

5.1 valueCtx结构

go 复制代码
type valueCtx struct {
    Context
    key, val any
}

valueCtx 同样继承了一个 parent context; 一个 valueCtx 中仅有一组 kv 对.

5.2 valueCtx.Value()方法

go 复制代码
func (c *valueCtx) Value(key any) any {
    if c.key == key {
        return c.val
    }
    return value(c.Context, key)
}
go 复制代码
func value(c Context, key any) any {
	//启动一个 for 循环,由下而上,由子及父,依次对 key 进行匹配
    for {
        switch ctx := c.(type) {
        case *valueCtx:
            if key == ctx.key {
                return ctx.val
            }
            c = ctx.Context
        //其中 cancelCtx、timerCtx、emptyCtx 类型会有特殊的处理方式
        case *cancelCtx:
            if key == &cancelCtxKey {
                return c
            }
            c = ctx.Context
        case *timerCtx:
            if key == &cancelCtxKey {
                return &ctx.cancelCtx
            }
            c = ctx.Context
        case *emptyCtx:
            return nil
        default:
            return c.Value(key)
        }
    }
}
  • 假如当前 valueCtx 的 key 等于用户传入的 key,则直接返回其 value;
  • 假如不等,则调用value方法从 parent context 中依次向上寻找,直到找到根节点emptyctx,若找不到则返回空。
  • 其中 cancelCtx、timerCtx、emptyCtx 类型会有特殊的处理方式,如当前key== &cancelCtxKey ,则会返回cancelCtx自身,而emptyCtx 则会返回空

5.3 context.WithValue()方法

go 复制代码
func WithValue(parent Context, key, val any) Context {
    if parent == nil {	//倘若 parent context 为空,panic;
        panic("cannot create context from nil parent")
    }
    if key == nil {		//倘若 key 为空 panic;
        panic("nil key")
    }
    if !reflectlite.TypeOf(key).Comparable() {	//倘若 key 的类型不可比较,panic;
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}	//包括 parent context 以及 kv对,返回一个新的 valueCtx结构体.
}

阅读源码可以看出,valueCtx 不适合视为存储介质,存放大量的 kv 数据,原因如下:

  • 一个 valueCtx 实例只能存一个 kv 对,因此 n 个 kv 对会嵌套 n 个 valueCtx,造成空间浪费;
  • 基于 k 寻找 v 的过程是线性的,时间复杂度 O(N);
  • 不支持基于 k 的去重,相同 k 可能重复存在,并基于起点的不同,返回不同的 v.

由此得知,valueContext 的定位类似于请求头,context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等.

6. 总结

6.1 总体类图

6.2 context注意事项

  1. 不要在结构类型中加入 Context 参数,而是将它显式地传递给需要它的每个函数,并且它应该是第一个参数,通常命名为 ctx:
  2. Context 是线程安全的,可以放心地在多个 goroutine 中使用。
  3. 当你把 Context 传递给多个 goroutine 使用时,只要执行一次 cancel 操作,所有的 goroutine 就可以收到 取消的信号
  4. 不要把原本可以由函数参数来传递的变量,交给 Context 的 Value 来传递。
  5. 当一个函数需要接收一个 Context 时,但是此时你还不知道要传递什么 Context 时,可以先用 context.TODO 来代替,而不要选择传递一个 nil。
  6. 当一个 Context 被 cancel 时,继承自该 Context 的所有 子 Context 都会被 cancel。

6.3 context使用案例

6.1 context.withCancel()使用案例

go 复制代码
package main

import (
context"
    "fmt"
    "time"
)

func HandleRequest(ctx context.Context) {
    go WriteRedis(ctx)
    go WriteDatabase(ctx)
    for {
        select {
        case <-ctx.Done():
            fmt.Println("HandleRequest Done.")
            return
        default:
            fmt.Println("HandleRequest running")
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func WriteRedis(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteRedis Done.")
            return
        default:
            fmt.Println("WriteRedis running")
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func WriteDatabase(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteDatabase Done.")
            return
        default:
            fmt.Println("WriteDatabase running")
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go HandleRequest(ctx)

    left := 10
    for i := 1; i < 10; i++ {
        left -= i
        time.Sleep(500 * time.Millisecond)
        if left < 0 {
            cancel()
            break
        }
    }
    //Just for test whether sub goroutines exit or not
    time.Sleep(5 * time.Second)
}

上面代码中协程HandelRequest()用于处理某个请求,其又会创建两个协程:WriteRedis()、WriteDatabase(),main协程创建context,并把context在各子协程间传递,main协程会计算left来模拟实际剩余的资源,当剩余的资源不足时可以cancel掉所有子协程。

6.2 context.WithTimeout()使用案例

go 复制代码
package main

import (
    "fmt"
    "time"
    "context"
)

func HandelRequest(ctx context.Context) {
    go WriteRedis(ctx)
    go WriteDatabase(ctx)
    for {
        select {
        case <-ctx.Done():
            fmt.Println("HandelRequest Done.")
            return
        default:
            fmt.Println("HandelRequest running")
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func WriteRedis(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteRedis Done.")
            return
        default:
            fmt.Println("WriteRedis running")
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func WriteDatabase(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteDatabase Done.")
            return
        default:
            fmt.Println("WriteDatabase running")
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func main() {
    ctx, _ := context.WithTimeout(context.Background(), 500 * time.Millisecond)
    go HandelRequest(ctx)

    time.Sleep(1 * time.Second)
}

同样的WriteRedis()、WriteDatabase()应用场景,但不是通过计算剩余的资源来cancel掉所有协程,而是判断当前的写入耗时,如果写入时间过长,则可通过WithTimeout()进行cancel。

6.3 context. WithValue()使用案例

go 复制代码
package main

import (
    "fmt"
    "time"
    "context"
)

func HandelRequest(ctx context.Context) {
    go WriteRedis(ctx)
    go WriteDatabase(ctx)
    for {
        select {
        case <-ctx.Done():
            fmt.Println("HandelRequest Done.")
            return
        default:
            fmt.Println("HandelRequest running, author: ", ctx.Value("Pistachiout"))
            time.Sleep(200 * time.Millisecond)
        }
    }
}
func WriteRedis(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteRedis Done.")
            return
        default:
            fmt.Println("WriteRedis running, author: ", ctx.Value("Pistachiout"))
            time.Sleep(200 * time.Millisecond)
        }
    }
}
func WriteDatabase(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("WriteDatabase Done.")
            return
        default:
            fmt.Println("WriteDatabase running, author: ", ctx.Value("Pistachiout"))
            time.Sleep(200 * time.Millisecond)
        }
    }
}

func main() {
    ctx, _ := context.WithTimeout(context.Background(), 500 * time.Millisecond)
    ctx2 := context.WithValue(ctx, "author", "Pistachiout")
    go HandelRequest(ctx2)

    time.Sleep(1 * time.Second)
}
相关推荐
研究司马懿16 小时前
【云原生】Gateway API高级功能
云原生·go·gateway·k8s·gateway api
梦想很大很大1 天前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
lekami_兰1 天前
MySQL 长事务:藏在业务里的性能 “隐形杀手”
数据库·mysql·go·长事务
却尘2 天前
一篇小白也能看懂的 Go 字符串拼接 & Builder & cap 全家桶
后端·go
ん贤2 天前
一次批量删除引发的死锁,最终我选择不加锁
数据库·安全·go·死锁
mtngt112 天前
AI DDD重构实践
go
Grassto4 天前
12 go.sum 是如何保证依赖安全的?校验机制源码解析
安全·golang·go·哈希算法·go module
Grassto5 天前
11 Go Module 缓存机制详解
开发语言·缓存·golang·go·go module
程序设计实验室6 天前
2025年的最后一天,分享我使用go语言开发的电子书转换工具网站
go
我的golang之路果然有问题6 天前
使用 Hugo + GitHub Pages + PaperMod 主题 + Obsidian 搭建开发博客
golang·go·github·博客·个人开发·个人博客·hugo