点击↑上方↑蓝色“编了个程”关注我~

af40cd48c9f4fc72d3633e8ddc66295f.png

这是Yasin的第 57 篇原创文章

12f1ef4bfe9b3015b370abc449227d03.png

Y说

周末的快乐时光总是很短暂。

今天天气不错,有点太阳。去附近的商场吃了一顿“高老九重庆火锅”,味道还行,主要是好久没吃火锅了~

白天把家里好好收拾了一下,感觉心情也跟着变好了。

已经用Golang在日常工作中开发了好几个月了。作为一个Golang菜鸟,有些东西往往只是会用,没有来得及去深究其背后的原理和设计用意。今年默默给自己立了一个Flag,就是好好深入学习一下这门语言。

context.Context

从协程说起

Golang这个语言的优势之一,就是它拥有一个高并发利器:goroutine。它是一个Golang语言实现的协程,单机就可以同时支持大量的并发请求,非常适合如今互联网时代的后端服务。

那有了大量的协程,就带来了一些问题。比如:请求的一些比较通用的参数(比如上面提到的Log Id)如何传递到协程呢?如何终止一个协程呢?

在Golang中,我们无法从外部终止一个协程,只能它自己结束。常见的比如超时取消等需求,我们通常使用抢占操作或者中断后续操作。

在context出来以前,Golang是channel + select的方式来做这件事情的。具体的做法是:定义一个channel,子协程启一个定时任务循环监听这个channel,主协程如果想取消子协程,就往channel里写入信号。

这样确实能解决这个问题,但编码麻烦不说,如果有协程里面启协程,形成协程树的话,就比较麻烦了,得定义大量的channel。

Context的接口

Contextcontext
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

简单解释一下四个方法的作用:

CanceledDeadlineExceeded

默认的Context实现

context

emptyCtx

emptyCtx的实现是一个int类型的变量,没有超时时间,不能取消,也不能存储任何额外信息。

BackgroundcontextcontextBackgroundTODOcontext

valueCtx

valueCtx可以存储键值对。且有一个指向父Context的组合。代码如下:

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

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}
WithValue

cancelCtx

canceler
type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     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
}

type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}
cancel
WithCancelcontextcancelCtxcontextWithCancelcontextCancelFuncCancelFunccancel
type CancelFunc func()

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    // 将当前context加入到最近的类型为cancelCtx的祖先节点的children中
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
    // 将parent作为父节点context生成一个新的子节点
    return cancelCtx{Context: parent}
}
propagateCancel

timerCtx

timerCtx是一种可以定时取消的context,内部是基于cancelCtx来设计的,也实现了cancel接口。

type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
    // 将内部的cancelCtx取消
    c.cancelCtx.cancel(false, err)
    if removeFromParent {
        // Remove this timerCtx from its parent cancelCtx's children.
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
        取消计时器
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}
WithDeadlineparentdeadlined
parentdcontextWithCancelcontextparentdtimerCtxcontextcontextddurdurtimerCtxDeadlineExceededtimerCtxtimerCtx
WithDeadlineWithTimeoutcontextWithDeadlineWithTimeouttimeout

使用

Done
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

// consumer
go func(ctx context.Context) {
    ticker := time.NewTicker(1 * time.Second)
    for _ = range ticker.C {
        select {
            case <-ctx.Done():
             fmt.Println("child process interrupt...")
             return
            default:
             fmt.Printf("send message: %d\n", <-messages)
        }
    }
}(ctx)
cancel()
net/http
valueCtxvalueCtxcontextvalueCtxcancelCtxcancelCtxcontextcancelCtxreqresponsecancelCtxcontext

关于第三步用代码解释可能更清晰一点:

ctx, cancelCtx := context.WithCancel(ctx)
req.ctx = ctx
// 省略其它方法
w = &response{
    conn:          c,
    cancelCtx:     cancelCtx,
    req:           req,
    reqBody:       req.Body,
    handlerHeader: make(Header),
    contentLength: -1,
    closeNotifyCh: make(chan bool, 1),

    // We populate these ahead of time so we're not
    // reading from req.Header after their Handler starts
    // and maybe mutates it (Issue 14940)
    wants10KeepAlive: req.wantsHttp10KeepAlive(),
    wantsClose:       req.wantsClose(),
}

这样设计有以下作用:

cancelCtxcancelCtxcancelCtx

总结&日常开发

contextcontext
contextcontextcontextcontextimmutable

可以看出来,Context最强大的功能就是可以优雅地关闭协程。在一般的服务框架中,这件事情可能就是框架帮我们做了,在接收请求之后设置一个context,传入到请求对应的协程里,在超时或者发生错误的时候调用cancel,关闭这个请求。需要注意的是,这里的请求协程一般是框架写代码去结束的。

WithTimeout

context的设计让我想起了「Java线程的中断」,它也是只是设置一个信号量,至于具体中不中断,是由线程根据具体的场景,自己决定的。之前也写过一篇Java线程中断方面的文章,感兴趣的小伙伴可以在公众号历史里面翻一翻。

_

由于context需要在函数一层层传递,所以有些同学编码的时候会觉得比较麻烦。在一门公司的内部课程里,提到一个方式,就是使用Java类似的ThreadLocal来存储context,在需要的时候去取。其中会用到一些黑科技,比如从stack上取goroutine的id这种。但我个人不是很建议这种方式,在设计context的时候,其实ThreadLocal已经存在了很久了。Golang为什么没有使用那种方式,而是采用了现在的设计,应该是有一定的用意的。Golang的context设计是遵循Golang本身函数式编程的思想的,如果使用ThreadLocal,感觉有些不伦不类了。

context也有值传递的功能。我们目前团队上只用来传了log Id,那是不是也可以用来传当前操作人信息呢?我觉得是可以的,大家可以根据自己的团队规范来统一使用~

参考:

知乎-深入理解Golang之context

Go语言中文网-golang中context包解读

Go语言中文网-服务器开发利器golang context用法详解

Go Concurrency Patterns: Context

https://go.dev/blog/context-and-structs

81e02d6560edc9a64f5b57c4500e63a5.png

关于作者

我是Yasin,一个爱写博客的技术人

微信公众号:编了个程(blgcheng)

个人网站:https://yasinshaw.com

欢迎关注这个公众号5872bd8159b3d281badb1ebd618d4d29.png

be5ac2a564e312f0331e1cb3c23cbf60.png

3d529f76eb98194815061302c4b1a308.png 5645366848a9f66dc68907233a896185.png