网上收集一些 golang context 的相关文章

1. context详解

在go服务器中,对于每个请求的request都是在单独的goroutine中进行的,处理一个request也可能设计多个goroutine之间的交互, 使用context可以使开发者方便的在这些goroutine里传递request相关的数据、取消goroutine的signal或截止日期。

Context是Golang官方定义的一个package,它定义了Context类型,里面包含了Deadline/Done/Err方法以及绑定到Context上的成员变量值Value

context 包定义了Context接口类型,它可以具有生命周期、取消/关闭的channel信号、请求域范围的健值存储功能。
因此可以用它来管理goroutine 的生命周期、或者与一个请求关联,在functions之间传递等。

每个Context应该视为只读的,通过WithCancel、WithDeadline、WithTimeout和WithValue函数可以基于现有的一个Context(称为父Context)派生出一个新的Context(称为子Context)。
其中WithCancel、WithDeadline和WithTimeout函数除了返回一个派生的Context以外,还会返回一个与之关联的CancelFunc类型的函数,用于关闭Context。

通过调用CancelFunc来关闭关联的Context时,基于该Context所派生的Context也都会被关闭,并且会将自己从父Context中移除,停止和它相关的timer。
如果不调用CancelFunc,除非该Context的父Context调用对应的CancelFunc,或者timer时间到,否则该Context和派生的Context就内存泄漏了。

可以使用go vet工具来检查所有control-flow路径上使用的CancelFuncs。

context的底层结构

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

这个就是Context的底层数据结构,来分析下:

字段 含义
Deadline 返回一个time.Time,表示当前Context应该结束的时间,ok则表示有结束时间
Done 当Context被取消或者超时时候返回的一个close的channel,告诉给context相关的函数要停止当前工作然后返回了。(这个有点像全局广播)
Err context被取消的原因
Value context实现共享数据存储的地方,是协程安全的(还记得之前有说过map是不安全的?所以遇到map的结构,如果不是sync.Map,需要加锁来进行操作)

同时包中也定义了提供cancel功能需要实现的接口。这个主要是后文会提到的“取消信号、超时信号”需要去实现。

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

那么库里头提供了4个Context实现,来供大家玩耍

实现 结构体 作用
emptyCtx type emptyCtx int 完全空的Context,实现的函数也都是返回nil,仅仅只是实现了Context的接口
cancelCtx type cancelCtx struct {…} 继承自Context,同时也实现了canceler接口
timerCtx type timerCtx struct {…} 继承自cancelCtx,增加了timeout机制
valueCtx type valueCtx struct {…} 存储键值对的数据

context的创建

为了更方便的创建Context,包里头定义了Background来作为所有Context的根,它是一个emptyCtx的实例。

var (
    background = new(emptyCtx)
    todo       = new(emptyCtx) // 
)

func Background() Context {
    return background
}

你可以认为所有的Context是树的结构,Background是树的根,当任一Context被取消的时候,那么继承它的Context 都将被回收。

2. context实战应用

2.1 WithCancel

实现源码:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent)
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}

实战场景:
执行一段代码,控制执行到某个度的时候,整个程序结束。

吃汉堡比赛,奥特曼每秒吃0-5个,计算吃到10的用时
实战代码:

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    eatNum := chiHanBao(ctx)
    for n := range eatNum {
        if n >= 10 {
            cancel()
            break
        }
    }

    fmt.Println("正在统计结果。。。")
    time.Sleep(1 * time.Second)
}

func chiHanBao(ctx context.Context) <-chan int {
    c := make(chan int)
    // 个数
    n := 0
    // 时间
    t := 0
    go func() {
        for {
            //time.Sleep(time.Second)
            select {
            case <-ctx.Done():
                fmt.Printf("耗时 %d 秒,吃了 %d 个汉堡 \n", t, n)
                return
            case c <- n:
                incr := rand.Intn(5)
                n += incr
                if n >= 10 {
                    n = 10
                }
                t++
                fmt.Printf("我吃了 %d 个汉堡\n", n)
            }
        }
    }()

    return c
}

输出:

我吃了 1 个汉堡
我吃了 3 个汉堡
我吃了 5 个汉堡
我吃了 9 个汉堡
我吃了 10 个汉堡
正在统计结果。。。
耗时 6 秒,吃了 10 个汉堡 

2.2 WithDeadline & WithTimeout

实现源码:

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    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) // deadline has already passed
        return c, func() { c.cancel(true, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

实战场景:
执行一段代码,控制执行到某个时间的时候,整个程序结束。

吃汉堡比赛,奥特曼每秒吃0-5个,用时10秒,可以吃多少个
实战代码:

func main() {
    // ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10))
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    chiHanBao(ctx)
    defer cancel()
}

func chiHanBao(ctx context.Context) {
    n := 0
    for {
        select {
        case <-ctx.Done():
            fmt.Println("stop \n")
            return
        default:
            incr := rand.Intn(5)
            n += incr
            fmt.Printf("我吃了 %d 个汉堡\n", n)
        }
        time.Sleep(time.Second)
    }
}

输出:

我吃了 1 个汉堡
我吃了 3 个汉堡
我吃了 5 个汉堡
我吃了 9 个汉堡
我吃了 10 个汉堡
我吃了 13 个汉堡
我吃了 13 个汉堡
我吃了 13 个汉堡
我吃了 14 个汉堡
我吃了 14 个汉堡
stop 

2.3 WithValue

实现源码:

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

实战场景:
携带关键信息,为全链路提供线索,比如接入elk等系统,需要来一个trace_id,那WithValue就非常适合做这个事。
实战代码:

func main() {
    ctx := context.WithValue(context.Background(), "trace_id", "88888888")
    // 携带session到后面的程序中去
    ctx = context.WithValue(ctx, "session", 1)

    process(ctx)
}

func process(ctx context.Context) {
    session, ok := ctx.Value("session").(int)
    fmt.Println(ok)
    if !ok {
        fmt.Println("something wrong")
        return
    }

    if session != 1 {
        fmt.Println("session 未通过")
        return
    }

    traceID := ctx.Value("trace_id").(string)
    fmt.Println("traceID:", traceID, "-session:", session)
}

输出:

traceID: 88888888 -session: 1

3. context建议

不多就一个。

Context要是全链路函数的第一个参数。

func myTest(ctx context.Context)  {
    ...
}

结构体/接口关系

从图中可以很容易看出:

1.Context主要包含了三个接口

Context接口
canceler接口
stringer接口
两个内部接口,一个外部接口

2.主要实现了三种结构体,其实就对应context的三种功能

1)同步取消功能

对应结构体cancelCtx,其重写了Context接口的三个方法:

Value
Done
Err

并实现了接口canceler,对应方法cancel和Done
也实现了接口stringer,对应方法String

2)超时控制功能

对应结构体timerCtx,其匿名内嵌了cancelCtx结构体,相当于继承了cancelCtx的所有方法
重写了Deadline方法

3)K-V功能

对应结构体valueCtx,其匿名内嵌了Context接口
重写了Value方法

对外函数

  1. WithCancel
    内部创建cancelCtx对象,返回时,强转为Context接口对象,并返回cancelCtx重写的cancel方法

  2. WithTimeout和WithDeadline
    两者是相同的功能,WithTimeout就是直接调用WithDeadline实现的
    其内部也是创建timerCtx对象(这个对象包含了新建的一个cancelCtx对象)
    由于timerCtx重写了cancel方法,所以返回的是timerCtx的cancel

  3. WithValue
    创建valueCtx对象,返回时,强转为Context接口对象
    并不返回cancel方法
    不过valueCtx重写了Value方法,所以返回的Context接口如果调用Value方法,就是调用的valueCtx实现的Value


由浅入深聊聊Golang的context
golang 上下文context用法详解

最后编辑: Simon  文档更新时间: 2021-08-30 20:59   作者:Simon