3W 解析 Golang 语言 Context:用法、源码

2023-06-29 0 176

原副标题:3W 导出 Golang 词汇 Context:用语、源标识符

3W 导出 Golang 词汇 Context:用语、源标识符

译者:thom,百度PCG前台合作开发技师

Context 是什么?

依照非官方表述在 go 词汇里面 context 包表述了两个 context 类别的USB,随身携带了止天数,中止讯号,延时天数,以及随身携带语句的控制系统模块(k-v)的类别。是 Go 词汇 1.7版导入的。

为何要有 context?

没人说 go1.7 版以后没 context,那为何 go1.7 版要导入 Context? 看 chatGPT 怎样提问:

只不过在 Go 1.7 以后,也有一种叫作 “context” 的监督机制,或者说它是专有的,根本无法在 Go 国际标准库外部采用。而在 Go 1.7 中,国际标准库将 context 监督机制申明了出来,使合作开发人员能在自己的标识符中采用它。 导入 context 的主要其原因是为了化解在mammalian程式设计中的许多难题,比如说在两个允诺预处置中,可能会开启数个 goroutine 来处置不同的各项任务,那些各项任务可能需要共享资源许多语句重要信息,比如说允诺的止天数、允诺的 ID 之类。如果没有两个国际标准化的监督机制来管理工作那些语句重要信息,那么标识符就会显得十分纷乱,无法保护。 通过导入 context,合作开发人员能将那些语句重要信息PCB在两个 context 第一类中,并将其传达给各 goroutine,进而实现了语句重要信息的国际标准化管理工作。如此一来,标识符就显得更为明晰、更易认知和保护了。

我归纳呵呵: 采用 context 管理工作语句 能使标识符更为简约和明晰,因此也防止了许多缓存安全可靠的信用风险。

context 是怎么做的? 没 context 那些难题是怎么做的?

没 context,数个 goroutine 之间要维持完全相同的延时天数和完全相同的表达式采用的是函数调用的形式。

package mainimport ( “fmt” “time”)// 函数调用,用作储存语句重要信息var ( deadline time.Time requestID string)func main { // 设置语句重要信息 deadline = time.Now.Add(5 * time.Second) requestID = “123456” // 开启两个 goroutine 来处置各项任务 go func { for { select { case <-time.After(1 * time.Second): // 模拟许多耗时的操作 fmt.Println(“goroutine 1: doing some work”) default: // 检查语句重要信息,如果已经延时或被中止了,就退出循环 if time.Now.After(deadline) { fmt.Println(“goroutine 1: context canceled”) return } } } } // 开启另两个 goroutine 来处置各项任务 go func { for { select { case <-time.After(1 * time.Second): // 模拟许多耗时的操作 fmt.Println(“goroutine 2: doing some work”) default: // 检查语句重要信息,如果已经超时或被中止了,就退出循环 if time.Now.After(deadline) { fmt.Println(“goroutine 2: context canceled”) return } } } } // 等待一段天数,然后中止语句重要信息 time.Sleep(3 * time.Second) fmt.Println(“main: context canceled”) deadline = time.Now time.Sleep(1 * time.Second)}

有了 context 之后,标识符如下。

package mainimport ( “context” “fmt” “time”)func main { // 创建两个带有止天数的 context ctx, cancel := context.WithDeadline(context.Background, time.Now.Add(5*time.Second)) defer cancel // 开启两个 goroutine 来处置各项任务 go func(ctx context.Context) { for { select { case <-ctx.Done: // 如果 context 被中止了,就退出循环 fmt.Println(“goroutine 1: context canceled”) return default: // 模拟许多耗时的操作,普通情况可能是rpc调用 time.Sleep(1 * time.Second) fmt.Println(“goroutine 1: doing some work”) } } }(ctx) // 开启另两个 goroutine 来处置各项任务 go func(ctx context.Context) { for { select { case <-ctx.Done: // 如果 context 被中止了,就退出循环 fmt.Println(“goroutine 2: context canceled”) return default: // 模拟许多耗时的操作 time.Sleep(1 * time.Second) fmt.Println(“goroutine 2: doing some work”) } } }(ctx) // 等待一段天数,然后中止 context time.Sleep(3 * time.Second) cancel fmt.Println(“main: context canceled”) time.Sleep(1 * time.Second)}

归纳:通过下面标识符实现实现随身携带延时天数的终止。同时标识符更为简约明晰容易保护。

ctx, cancel := context.WithDeadline(context.Background, time.Now.Add(5*time.Second)) … ctx.Done

能替换每两个地方的止天数,同时这个 ctx 是只读的,不会有修改。同时如果有其他的变更是带锁的操作,contetextUSB是提供的只读的方法。

time.Now.After(deadline)

因此我觉得是两个能力的替换,采用 context 保存 deadline 函数调用,只提供只读操作,因此通过PCB起来,里面的锁监督机制保证缓存安全可靠。

Context 是怎么做到的包含 goroutinue 的语句?

试想呵呵如果让你来设计数个协程控制延时天数,你会怎么设计和怎么走? 也是两个函数调用一直玩下传达? 前面提到了函数调用的方法,但是在可保护性,缓存安全可靠之类都有许多难题。 协程之间同时控制国际标准化完成。 正常的go服务里面,通常要控制数个协程进行操作,这个时候就需要两个context来进行管理工作,因此非官方也推荐ctx context.Context放在第两个入参。

3W 解析 Golang 语言 Context:用法、源码

那就要跟我们读呵呵 context 的源标识符,深入探索那些原理了。 首先 Context 怎样将延时天数一层一层传达给上面? 如下标识符所示:对于 Context 在包内是两个USB,表述了 4 个方法,因此都是幂等的。

// Conetext 包介绍 : 通常context随身携带止天数,**和中止讯号**,以及其他跨越API边界的值,Context的方法能被数个协程同时调用。package contexttype Context interface { // 返回止的日期,如果无止日期,ok返回false Deadline (deadline time.Time, ok bool)// 返回两个channel,当工作已完成或者语句被中止时关闭。如果是两个不会被中止的语句,Done会返回nil// WithCancel方法,会在被调用cancel时,关闭Done// WithDeadline方法,会在过止天数时,关闭Done// WithTimeout方法,会在延时结束时,关闭Done Done <-chan struct{}// Done没被关闭时,会返回nil// 如果Done关闭了,将会返回关闭的其原因(中止、延时)Err error// 返回与当前语句关联的键值或nil。如果没值与键关联,采用完全相同键连续调用 Value 会返回完全相同的结果Value(key interface{}) interface{}}

因为当前自己的版是 go1.20 版,本次所讲到的源标识符就是 go 1.20.

Context 的延时天数控制

我们采用 context 采用的最多也是约定俗成的就是通过 ctx 控制协程之间的延时天数了,那么我们看下源标识符是怎么实现的?

我们由于前面的例子中能看到延时天数是通过 ctx.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{})}

c.done 是“懒汉式”创建,只有调用了 Done方法才被创建,因此能看到是两个只读的 channel,因此没地方向 channel 里面写数据。因此调用读这个 channel,协程会被 block 住。通常搭配 select 来采用。一旦关闭,就会立刻读出零值。 那我们推测出来,肯定是在 WithCancel 来进行关闭或者 WithDeadLine超过了定时 timer 进行关闭的。因此要递归关闭掉所有的父节点和子节点。 我们通过 debug 跳到源标识符里面去跟踪呵呵源标识符: 首先是 context.WithDeadline

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) { // The current deadline is already sooner than the new one. return WithCancel(parent) } c := &timerCtx{ cancelCtx: newCancelCtx(parent), deadline: d, } propagateCancel(parent, c) dur := time.Until(d) if dur <= 0 { c.cancel(true, DeadlineExceeded, nil) // deadline has already passed return c, func { c.cancel(false, Canceled, nil) } } c.mu.Lock defer c.mu.Unlock if c.err == nil { c.timer = time.AfterFunc(dur, func { c.cancel(true, DeadlineExceeded, nil) }) } return c, func { c.cancel(true, Canceled, nil) }}

根据上面能知道,所有 close(channel)都在 c.cancel 里面。 我们看下 cancel的实现?

func (c *timerCtx) cancel(removeFromParent bool, err, cause error) { c.cancelCtx.cancel(false, err, cause) if removeFromParent { // Remove this timerCtx from its parent cancelCtxs children. removeChild(c.cancelCtx.Context, c) } c.mu.Lock if c.timer != nil { c.timer.Stop c.timer = nil } c.mu.Unlock}

归纳下上面标识符: cancel的实现的方法功能是关闭 channel:c.done;递归地中止他所有的子节点,从父节点删除自己。能达到的效果就是关闭 channel.将中止讯号传达给所有子节点。 综上我们能看出来,是怎样通过 context 控制整个链路上的延时天数和控制所有节点同两个天数推出和关闭通道。 我们利用了通道特性和PCB和递归做到了简单的控制了 rpc 之间的调用关系。

通过 ctx 传达控制系统模块共享资源数据

源标识符:

func WithValue(parent Context, key, val any) Context { if parent == nil { panic(“cannot create context from nil parent”) } if key == nil { panic(“nil key”) } if !reflectlite.TypeOf(key).Comparable { panic(“key is not comparable”) } return &valueCtx{parent, key, val}}

例子:

package mainimport ( “conteig{LogLevel: “debug”, Timeout: 100} } return config}func main { // 初始化两个 context ctx := context.Background // 设置控制系统配置重要信息到 context 中 config := Config{LogLevel: “info”, Timeout: 200} ctx = context.WithValrintln(“Timeout:”, c.Timeout)}

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) } }}

上面我们能看到能享函数调用十分方便,但是他存在也存在许多缺点,就是函数调用的通病。不知道在哪里修改的,另外我们看到 context.WithValue,每两个加一层,类似链表,通常都是圈复杂度是 0(n)如果没控制很好,效率不高。 比如说下面:

ctx = context.WithValue(ctx, “config”, config) ctx = context.WithValue(ctx, “test”, “1

能看到如果赋值3次,那么可能需要递归3次,才能去得到表达式。因此复杂度是0(n)

Context 缺点

通过上面的例子我们也能看到 Context 通过 context 的包以及PCB让我们写服务标识符更为简单和精炼,那么真的 Context 有什么缺点呢? 只不过前面源标识符以及分析已经有提到了。

从源标识符的角度来看 WithValue WithDeadline 等方法存在链表嵌套复杂度比较高 如果滥用标识符比较无法为保护 如果不认知 context,标识符不是很好认知(这也是其中的两个小优点,大家约定context做的事情) 传达的数据根本无法是基本的数据类别或者引用。!reflectlite.TypeOf(key).Comparable如果不是可比较的 key 就 panic 了。 如果 context 传达比较耗时,要保证及时清理 context 传达的重要信息。 归纳

我们这里讲到了 Context 是什么?在 go 词汇里面 context 就能认知是传达上下文重要信息的 interface。为何要有 Context 呢?我归纳到的比较简单就是在 server 以后的协程调用的情况,传达许多函数调用要考虑安全可靠性,生命周期,简约型,这个时候通过 context 在缓存安全可靠的情况下化解那些难题。然后通过源标识符阅读认知在没 context 以后和有 context 之后化解这个难题的区别,以及为何通过 context 能化解这个难题,深入认知 cancel,WithCancel 方法,WithDeadline 那些函数来化解这个难题,同时也归纳了 WithValue 随身携带语句的标识符原理。以及采用 context 的优缺点。以及我们在采用 context 的时候要注意哪些东西。 上述归纳可能并不一定完全正确,如果大家发现难题请批评指正。

相关文章

发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务