G

[Golang基础] Context的用法

RoLingG Golang 2024-04-25

Context

介绍

如它的字面意思,Context 可用来传输数据。但它还有一个别的功能,就是用来取消一个协程的执行。但总之,Context根源是基于上下文的这样一个东西。

传输数据功能

Context 本身是个 interface 类型的结构体,它有四个方法:Deadline()Done()Error()Value()

  • Deadline() 用于获取 Context 的截止时间
  • Done() 用于返回一个只读的 Channel ,用于通知当前 Context 是否已经被取消。
  • Err() 用于用去 Context 取消的原因
  • Value() 用于获取 Context 中存储的键值对数据
//Go语言源码内
type emptyCtx int

//当这个上下文没有过期时间的时候,可以通过后面这个bool值标明。
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return    //空白上下文,直接返回默认时间和false,false会导致这个上下文永不结束
}

//返回一个只读的channel,这个channel可以作为一个信号发射器来反映context是否停止。
//我们知道go语言中的struct本身是一个空类型,所以channel不做数据存储用途。
//上面这一条也证实是用来作为一个通道信息的信号发射器。
func (*emptyCtx) Done() <-chan struct{} {
    return nil    //因为nil,会导致任何想要读取emptyCtx内容都会阻塞(因为Channel的特性)
}

//context生命周期终止返回一个err。
//这个方法利于通过同步调用方式直接感知context现在是什么情况。
func (*emptyCtx) Err() error {
    return nil    //空白上下文的err直接是空,也就是没有错误
}

//这个方法是context数据存储能力的支撑,通过key获取value。
func (*emptyCtx) Value(key any) any {
    return nil    //空白上下文的value返回空,也就是没有任何的数据存储在里面
}

从上面我们会发现,官方给了我们一个空的上下文emptyCtx,这个空的上下文就和一张空白白纸一样,上面啥都没,没有任何的属性。

实际我们创建的每一个Context,它们的祖辈Context一定是这一个emptyCtx

这个emptyCtx虽然实际上啥都没有,但也实现了一个最基本的Context架构。

我们之后的任何Context操作,都是在这一个最基本的架构上进行叠加实现的。

也就是说,一个父Context可以作为一个加了新功能的子Context的基底,它们之间是有基底联系的。一个父Context可有有多个加有不同功能的子Context。所以Context一定会形成一个类似树状的结构,那个最开始的父Context(也就是emptyCtx)就是这棵树的根节点。

当有一个Context生命周期终止了,那么它作为父节点的下面的子 Context就会被父Context单向传递终止事件,使它的子Context们也跟着终止,子Context的孙Context们也会跟着终止,以此类推。

注意:这个过程只能是单向的从父→子。

因为上面这种传递终止的特性,使Context得以服务于父子协程的并发控制,让这些协程知道什么时候该结束,以及触发结束的条件是什么。

但这些都是Context取消功能的事,不属于传递数据功能的事,后话再说(

context.backgroudcontext.todo 都是用于创建上下文,都是属于 emptyCtx

//Go语言源码内
type emptyCtx int
var (
   background = new(emptyCtx)
   todo       = new(emptyCtx)
)
// 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 具体是怎样的:

func main() {
    ctx := context.Background()
    ctx = context.WithValue(ctx, "name", "RoLingG")
    getContext(ctx)

}

func getContext(ctx context.Context) {
    fmt.Println(ctx.Value("name"))
}

因为 WithValue() 传的是 any 类型,所以甚至能在 value 处传递一个结构体。

type userName struct {
   Name string
}

func main() {
   ctx := context.Background()
   //ctx = context.WithValue(ctx, "name", "RoLingG")
   ctx = context.WithValue(ctx, "name", userName{Name: "RoLingG"})
   getContext(ctx)

}

func getContext(ctx context.Context) {
   fmt.Println(ctx.Value("name").(userName).Name)    //使用断言解析结构体的内容
}

一个协程,当你不知道什么时候终止它,那就不要创建它。

​ ——不知道谁说的,但算是个准则。

这句话背后的含义就是:不要滥用并发。

要使用并发的时候,就要有并发控制这一概念,要去有目的的使用并发。无目的的滥用并发,可能会引起各种性能问题,严重的可能引起协程泄露。

所以Context 有三种取消方式去解决并发控制问题。

取消功能

Context 一共有三种取消方式:①取消协程、②截止时间取消 ③超时时间取消

通过Context可以有效的帮我们控制协程的资源,当一个协程不符合我们的需求的时候便可以使用Context去取消掉它从而达到回收资源的效果。

  • 取消协程withCancel

    例如一个app内执行的功能的取消按钮执行的功能,Context的取消协程就类似这个例子,可以将正在执行的协程取消掉。

    var wait = sync.WaitGroup{}
    
    func getIP(ctx context.Context) (ip string, err error) {
        go func() {
            select {
            case <-ctx.Done():
                fmt.Println("协程被取消", ctx.Err()) //模拟实际网络超时,超过取消检测时间,导致协程被取消,释放资源
                err = ctx.Err()
                wait.Done()
                return
            }
        }()
        time.Sleep(4 * time.Second) //之所以上面协程getIP没有获取到ip是因为这里时间写死了,没能模拟到实际网络情况,如果实际流畅的话这里的时间很短,是可以获取到IP的
        ip = "127.0.0.1"
        wait.Done()
        return
    }
    
    func main() {
        t1 := time.Now()
    
        ctx, cancel := context.WithCancel(context.Background()) //创建取消协程的上下文
    
        wait.Add(1)
        go func() {
            ip, err := getIP(ctx) //让取消协程的上下文作为参数进行控制
            //if err != nil {
            //    return
            //}
            fmt.Println(ip, err)
        }()
    
        go func() {
            time.Sleep(2 * time.Second)
            //取消协程
            cancel() //使用取消协程的方法进行取消对应的协程,因为ctx被上面那个协程使用,所以这里的cancel()是取消上面的协程
        }()
        wait.Wait()
        fmt.Println("执行成功", time.Since(t1))
    }
  • 截止时间取消WithDeadline

    在超时的情况下取消请求。会将相对时间转换为绝对时间。也就是传入一个结束时间点。

    func main() {
    
        var wg = sync.WaitGroup{}
    
        t1 := time.Now()
    
        ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second)) //创建取消协程的上下文
    
        wg.Add(1)
        go func() {
            ip, err := getIP1(ctx, &wg)    //触发截止时间取消上下文,截止后超过2s立刻取消该协程
            fmt.Println(ip, err)
        }()
        wg.Wait()
        fmt.Println("执行成功", time.Since(t1))
    }
    
    func getIP1(ctx context.Context, wg *sync.WaitGroup) (ip string, err error) {
        go func() {
            select {
            case <-ctx.Done():
                fmt.Println("协程超时", ctx.Err()) //模拟实际网络超时,超过检测时间,导致协程被取消,释放资源
                err = ctx.Err()
                wg.Done()
                return
            }
        }()
        time.Sleep(4 * time.Second) //4秒超2秒检测,超时取消协程
        ip = "127.0.0.1"
        wg.Done()
        return
    }
  • 超时时间取消WithTimeout

    用法与截止时间基本一致,本身就是基于截止时间去实现的,但协议不一样,还要传入一个距离当前时刻的相对的时间。也就是说传入一个持续时间。

    func main() {
    
        var wg = sync.WaitGroup{}
    
        t1 := time.Now()
    
        ctx, _ := context.WithTimeout(context.Background(), 2*time.Second)
    
        wg.Add(1)
        go func() {
            ip, err := getIP2(ctx, &wg) //触发截止时间取消上下文,协程运行超过2s则立刻取消该协程
            fmt.Println(ip, err)
        }()
        wg.Wait()
        fmt.Println("执行成功", time.Since(t1))
    }
    
    func getIP2(ctx context.Context, wg *sync.WaitGroup) (ip string, err error) {
        go func() {
            select {
            case <-ctx.Done():
                fmt.Println("协程超时", ctx.Err()) //模拟实际网络超时,超过检测时间,导致协程被取消,释放资源
                err = ctx.Err()
                wg.Done()
                return
            }
        }()
        time.Sleep(4 * time.Second) //4秒超2秒检测,超时取消协程
        ip = "127.0.0.1"
        wg.Done()
        return
    }

后日谈:

实际上这里应该还涉及的了cancelCtxtimerCtxvalueCtx这些底层的东西,具体的话还是去找找别人的资料吧,我也才刚学。

用于取消协程的功能时:

cancelCtx一定要作为一个子Context而存在,它必须要有一个父Context作为支撑。

timerCtx其实很大程度上是基于cancelCtx的基础上完成操作的,它的源码层面就继承了cancelCtx,并且重写了cancel方法。所以timerCtx一定程度上依附着cancelCtx

细看cancelCtx

cancelCtx的源码:

type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
    cause    error                 // set to non-nil by the first cancel call
}

请输入图片描述

  • 源码中我们第一个看Context,它很好的印证了“cancelCtx一定要作为一个子Context而存在,它必须要有一个父Context作为支撑”这句话。
  • 第二个看mu,这是一个互斥锁,当在并发环境下,并发操作一个资源时,可以通过这个互斥锁去保证操作的一致性。
  • 第三个看done,这是一个只读的channel,这个和上面的emptyCtxdone几乎是一样的,用于感知当前这个Context生命周期的变化。
  • 第四个看children,这是一个指向当前Context的子Context的集合,里面的 map[canceler]canceler 是一个interface 类型,也就是说是一个数据接口,它的任何子Context新添加的功能都与当前这个父Context无关。当父Context生命周期结束关闭时,子Context添加的新功能实现应该由它自己本身去解决。(大概是这样吧,第一次看源码,还不太懂)
  • 第五个看err,当前Context生命周期终止时,err被用来将错误对象会被传递给使用此上下文的代码,以便其知道上下文已经被取消。它提供了一种通用的方式来传递取消操作的信号,并且通常包含了关于取消操作本身的信息。
  • 在更新版本超过Go 1.19之后,Context中的cancleCtx添加了一个新的对象cause,这是为了帮助开发人员更好地理解和调试问题。

    第六个cause 字段提供了更具体的原因或根源信息,用于解释为什么上下文被取消。它可能包含关于取消操作背后更详细的上下文或场景。在某些情况下,cause 字段可能包含原始的错误信息或异常的堆栈跟踪,以提供给开发人员更深入的洞察力。

    这里就体现出了Go语言的特性,Go 语言中之所以没有像其他语言一样有继承的概念,就是为了将父与子区分开,两个该干嘛干嘛,谁实现的东西谁去管理,而不是子新实现的东西交给父来管理,划分清楚边界,职责内聚

cancelCtx的Deadline()

type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
    cause    error                 // set to non-nil by the first cancel call
}

根据cancelCtx的源码,我们会发现cancelCtx并没有Deadline()这个方法。所以如果要调用这个方法,cancelCtx会直接叫父Context去实现这个方法。如果没有的话,就返回一个初始的时间和一个false的bool值。

cancelCtx的done()

func (c *cancelCtx) Done() <-chan struct{} {
    d := c.done.Load()
    if d != nil {
        return d.(chan struct{})
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    d = c.done.Load()
    if d == nil {
        d = make(chan struct{})
        c.done.Store(d)
    }
    return d.(chan struct{})
}

cancelCtxdone()方法,我们通过源码可以看出除了与普通的Context一致,都是读取使用cancelCtx的这个channel以外,还有一个互斥锁,这是为了保证channel懒加载(初始化channel存储到aotmic.Value当中,并返回)和在并发环境下的一致性操作。如果去除掉并发环境下,那这个代码就是简单的获取channel,检查这个channel是否存在,存在则返回。

cancelCtx的err()

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    err := c.err
    c.mu.Unlock()
    return err
}

cancelCtxerr()这一个方法,也会存在并发读写的情况,所以在普通Contexterr()方法读取cancelCtx.err的基础上也加了一个互斥锁。

cancelCtx的cause的相关方法

至于cause,我们看cancelCtx的源码:

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
    cancel(removeFromParent bool, err, cause error)
    Done() <-chan struct{}
}

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
// cancel sets c.cause to cause if this is the first time c is canceled.
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    if cause == nil {
        cause = err
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    c.cause = cause
    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, cause)
    }
    c.children = nil
    c.mu.Unlock()

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

会发现它实际上用在cancel()这个方法中,cause 的存在是为了提供比 err 更详细的信息。err 通常只是一个通用的错误对象,表示上下文已被取消,而 cause 则提供了导致取消操作发生的具体原因或背景。这对于调试和错误处理非常有用,因为它可以帮助开发人员更深入地了解为什么上下文被取消。

cancelCtx的value()

func (c *cancelCtx) Value(key any) any {
    if key == &cancelCtxKey {
        return c
    }
    return value(c.Context, key)
}

根据源码来看,倘若key为特定值&cancelCtxKey,那么就返回cancelCtx自身的指针。如果不是,则遵循valueCtx的思路取值返回。

注意:这个&cancelCtxKey是不可导出的类型,所以不可能是外部的用户写的,一定是在本程序代码内声明出来调用的。

cancelCtx的withCancel()

// WithCancelCause behaves like WithCancel but returns a CancelCauseFunc instead of a CancelFunc.
// Calling cancel with a non-nil error (the "cause") records that error in ctx;
// it can then be retrieved using Cause(ctx).
// Calling cancel with nil sets the cause to Canceled.
//
// Example use:
//
//    ctx, cancel := context.WithCancelCause(parent)
//    cancel(myError)
//    ctx.Err() // returns context.Canceled
//    context.Cause(ctx) // returns myError
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
    c := withCancel(parent)
    return c, func(cause error) { c.cancel(true, Canceled, cause) }
}

func withCancel(parent Context) *cancelCtx {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    c := newCancelCtx(parent)
    propagateCancel(parent, c)
    return c
}

从上述源码可以看出withCancel()方法中的cancelCtx的父Context不能为空。不为空之后将父Context注入一个新的Context中,这样就获得了一个新的子ContextpropagateCancel(parent, c)传播取消信号保证了如果父Context被取消了,那么它的子Context也要跟着被取消。

然后新的子Context返回给WithCancelCause(),往下走就到return c, func(cause error) { c.cancel(true, Canceled, cause) },这代表WithCancelCause()最终连带返回了一个以终止该cancelCtx的闭包函数。只要调用这个闭包函数,withCancel保证创建出来的新子Context就会被取消,同时以它作为父Context的所有子Context也会因为取消信号一并被取消。

cancelCtx的newCancelCtx()

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

这个方法被调用就很根据传入的父Context去创建一个新的子cancelCtx,它的父Context即为传入的这个parent Context

cancelCtx的propagateCancel()

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
    done := parent.Done()
    if done == nil {
        return // parent is never canceled
    }

    select {
    case <-done:
        // parent is already canceled
        child.cancel(false, parent.Err(), Cause(parent))
        return
    default:
    }

    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err, p.cause)
        } else {
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        goroutines.Add(1)
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err(), Cause(parent))
            case <-child.Done():
            }
        }()
    }
}

根据之前的描述我们可以知道propagateCancel()这个方法就是为了传播取消信号。我们根据源码一行行看下来会发现它其实有对父Contextdone()方法传出来的值做校验,还有挺多的判断,这些判断能为它很好的选出情况并做出对应的解决方法。

parent.Done()nil的时候,那么代表这个父Context永远都不会被取消终止,那也就意味着没有必要去进行取消传播。

当父Context已经开始被取消时,那么子Context也必须跟着取消,子Context直接调用cancel进行取消,err用父Context的,来源也于父Context

接着往下,当父Context也是cancelCtx,那么只需要保证子Context能添加进父cancelCtxchildren集合中去就行。这样能基于cancelCtx自身的取消机制,连着子cancelCtx集合一并取消。

假如父Context不是cancelCtx,那么它会在propagateCancel()内开启一个异步的协程,使用一个select的多路复用框架,时刻检测父与子的channel,如果父Context生命周期终止,就能从父Context中获取到取消信号,那么同时让子Context使用cancel()就能一起取消掉了;如果子Context比父Context更早被取消,那么就啥都不用做,因为取消信号不能逆向而上,只能单向向下。

但这如何判断父ContextcancelCtx呢,答案就在下面。

cancelCtx的parentCancelCtx()

func parentCancelCtx(parent Context) (*cancelCtx, bool) {
    done := parent.Done()
    if done == closedchan || done == nil {
        return nil, false
    }
    p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
    if !ok {
        return nil, false
    }
    pdone, _ := p.done.Load().(chan struct{})
    if pdone != done {
        return nil, false
    }
    return p, true
}

从上往下看源码,一开始还是一如既往地判断是有父Context为空,因为cancelCtx必须要父Context,所以为空就不用判断了,直接返回。

我们接着往下看,我们会发现这里用到了前面value()中的&cancelCtxKey,这里其实我们就可以明白了源码是如何来判断父Context是否是cancelCtx了,就是通过这个特殊的指针&cancelCtxKey。对于cancelCtx而言,在调用value()这个方法,时,会返回它自身,也就是说cancelCtxkey必为&cancelCtxKey

那么,如果通过p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)这条代码检测得出oktrue,那么通过if !ok条件进行判断不成立的话,那么父Context就一定是cancelCtx

cancelCtx的cancel()

我们该如何去取消当前的Context,并保证上游的Context也能够接收到,这一点官方的源码也写的很详细:

// WithCancelCause behaves like WithCancel but returns a CancelCauseFunc instead of a CancelFunc.
// Calling cancel with a non-nil error (the "cause") records that error in ctx;
// it can then be retrieved using Cause(ctx).
// Calling cancel with nil sets the cause to Canceled.
//
// Example use:
//
//    ctx, cancel := context.WithCancelCause(parent)
//    cancel(myError)
//    ctx.Err() // returns context.Canceled
//    context.Cause(ctx) // returns myError
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
    c := withCancel(parent)
    return c, func(cause error) { c.cancel(true, Canceled, cause) }
}

func withCancel(parent Context) *cancelCtx {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    c := newCancelCtx(parent)
    propagateCancel(parent, c)
    return c
}

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    if cause == nil {
        cause = err
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    c.cause = cause
    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, cause)
    }
    c.children = nil
    c.mu.Unlock()

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

这里我们要连着withCancel()这个方法去看,在当前Context要取消时,它需要给它自身的err存入被取消的err,同时让cause存入err用于帮助开发人员查询信息。同时还要保证当前的chan能被上游的Context调用done()方法能够读取到信号,而不是被阻塞。cancelCtx有义务将它下游的所有子Context在它被取消时也同时被取消。

细看valueCtx

Context用于存储功能时:

无标题-2024-04-24-2215

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

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

func value(c Context, key any) any {
    for {
        switch ctx := c.(type) {
        case *valueCtx:
            if key == ctx.key {
                return ctx.val
            }
            c = ctx.Context
        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一定会有一个父Context,不能当祖节点。

我们可以看看源码,会发现valueCtxkey/value不是map,而是一个key-value对。设置了多个key-value对,就代表有多个作为子节点的valueCtx产生。

如果我们调用Value()方法,它会去匹配当前这个Contextkey是否为传入的key相互匹配。如果匹配上,则直接返回;如果不行,则而外调用value()方法进一步匹配。我们看value()方法里就会发现其实valueCtxcancelCtx是有强相关关系的,在switch的选择里会去判断当前的Context是否是cancelCtx

我们但看valueCtx的情况,源码缩写大概就是下面这样:

for {
        switch ctx := c.(type) {
        case *valueCtx:
            if key == ctx.key {
                return ctx.val
            }
            c = ctx.Context
        default:
            return c.Value(key)
        }
    }

我们会发现如果当前的ContextvalueCtx那么在Value()没匹配到的情况下,value()它会不断地循环往上拿父Context去匹配。这是一个从子→父找的过程,直至匹配成功或者失败。

时间复杂度会有问题,一般为O(N),因为是类链路的形式走,也就是线性行走,不是常数级的时间复杂度。另外每一次创建一个新的valueCtx的代价都比较昂贵,因为做的东西可能少,但是创建的确实一整个利用率不高的valueCtx。所以valueCtx不能当做是存放业务数据的存在,而是类似于header一样的存在,只存放少量作用大的全局mate数据。

还有,当我们两次为一个相同的key放入值的时候,它不会有去重的机制,而是生成两个新的不同的子valueCtx去存储这两个值。我们之后去寻址的时候,寻找这个key那么这个key返回的结果是不确定的,取决于调用context.value的节点在哪,在的节点有key对应的value就会就近取它;但如果没有,则会跑到当前节点的父节点去匹配,以此类推。

请输入图片描述

例如:valueCtx要找键值为c的,我们从键值为fContext开始找,往上找一个父节点就能找到了。但如果我们从键值为e的节点开始找,就不可能找到,直接一直往上直至全部匹配失败。

但这不是Context的一个缺点,相反它是故意这么做的。因为Context是作用于并发的场景中,我们都知道并发读操作是没有问题的,但如果参入了并发写,那就会出问题了。

这样故意分散开key的对应值就会使其即便在并发的环境中,也不用担心并发写的问题。因为数据都被打散在不同的节点中,不会相互影响。保证了这些key-value对是不同子协程中独有的数据。

当然也可以通过withValue()方法进行不同协程之间共享key-value对。

PREV
[每日算法] 区间加法②
NEXT
[Golang]Gin框架 Swaggo踩坑记录

评论(0)

发布评论