浅挖上下文context的设计实现 | 草台班子

LOADING

加载过慢请开启缓存 浏览器默认开启

浅挖上下文context的设计实现

2024/9/26 Go 随想

参考Go语言的设计实现(看完后才发现作者是k8s的contributor,膜拜)
但是因为go的版本不同源码有点小小的变换,整体思路是一致的。


学go也有大半个月了,速通了基础语法和gin,做了IM和shorter-url练手,感觉不错。但最近整体还是很迷茫,做好了考研的打算但还是放不下自己的三脚猫功夫。反正时间还早,索性就跟着兴趣学了。
刚好前段时间看了小徐先生的Golang打怪升级之路
他在筑基篇提到的大多数技术我都在Java中有所了解,唯有context除外。而Gin中context也是频繁出现

设计原理

Go语言设计与实现的定义中,上下文 context.Context是

用来设置截止日期、同步信号,传递请求相关值的结构体

单看context的源码也能看出来

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}

有日期,有channel,还有error。这里的any类型实质上还是interface{},可以存放任意类型的值。
Go语言设计与实现中又说

在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。
Go 服务的每一个请求都是通过单独的 Goroutine 处理的2,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。

这里goroutine的底层我还没有能力看,只用知道context是go协程的同步有用。在一次请求中,我们可能会创建多个goroutine,这些协程之间需要同步的数据,context因此产生,“上下文”用来形容真的是再好不过了
附一张大佬的图
img.png

常用实现

这段时间的学习中,go中最常用的context实现大概是context.Backgroundcontext.TODO,看了下源码,二者其实又没有区别,都是返回了一个emptyCtx,也就是一个空的context。
早期的go语言中这里传的是*emptyCtx,现在已经改成了emptyCtx。毕竟空结构体占用内存很小,直接传值观感上更加方便
img.png
可以看到emptyCtx也是通过空方法实现了 context.Context 接口中的所有方法,它没有任何功能。正是如此,我们才能自由发挥。
Gin中的gin.Context也视为标准库的 context.Context 的一个超集,是松子酒基石(想写自己的goweb框架是不是也要从这方面入手?以后再看看其他框架是怎么做的吧)

取消方法

有始有终,常用的context取消方法几乎都是WithXXX(WithValue除外),原理都一样。 摘取其中一个的解释

context.WithCancel 函数能够从 context.Context 中衍生出一个新的子上下文并返回用于取消该上下文的函数。
一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号。

取消信号的底层函数

func (c *cancelCtx) propagateCancel(parent Context, child canceler)

传值方法

Go语言设计与实现的作者所说,传值的功能很少用到,仅作了解。

在真正使用传值的功能时我们也应该非常谨慎,使用 context.Context 传递请求的所有参数一种非常差的设计,
比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。

Gin也是对context做了许多处理才拿来用的
上面提到的WithValue就是传值方法,它可以将任意值绑定到上下文中,供后续使用。但是最后返回的值还是valueCtx的指针,valueCtx实现如下

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 或者查找到对应的值。

写到这里已经是9.27深夜了,这篇blog只是对大佬的文章做了个记录,毕竟我能看懂已经很费力了。。。
今天观摩了华哥的恒生笔试,压力山大,不知道一年后的自己是在写考研题还是笔试题?昨天又和达哥聊了很多,感觉更大可能是前者吧
打把lol睡了


附:context小练手源码

package main

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

func RequseThirdApi(ctx context.Context, userId int) (bool, error) {
    time.Sleep(time.Second * 1)
    if errors.Is(ctx.Err(), context.DeadlineExceeded) {
        return false, errors.New("context time out!")
    }

    return true, nil
}

func main() {
    ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*2)

    defer cancelFunc()

    if isUser, err := RequseThirdApi(ctx, 021331); err != nil {
        fmt.Println("user err!", err)
    } else {
        fmt.Println("user success!", isUser)
    }

}```