为了账号安全,请及时绑定邮箱和手机立即绑定

Golang的context包详解

标签:
Go


context 包说明

说明:本文的用到的例子大部分来自context包。

概述

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时,建议遵循如下规则:

1、不要将Context存储为结构体的字段,应该通过函数来传递一个具体的Context。并且Context应该放在第一个参数,变量名为ctx。比如

func DoSomething(ctx context.Context, arg Arg) error {

    // ... use ctx ...

}

2、即使函数允许,也不要传递nil。如果你不确定Context的使用,你可以传递context.TODO。

3、请求域参数应该通过Context上的K/V方式传递,不要通过函数参数传递具体的请求域参数。

Context可能会在多个goroutines之间传递共享,它是例程安全的。

可以参考https://blog.golang.org/context 中的服务器使用例子。

包导入

在 go1.7 及以上版本 context 包被正式列入官方库中,所以我们只需要import "context"就可以了,而在 go1.6 及以下版本,我们要 import "golang.org/x/net/context" 。

Context 接口

context.Context接口的定义如下:

// Context 的实现应该设计为多例程安全的

type Context interface {

    // 返回代表该Context过期的时间,和表示deadline是否被设置的bool值。

    // 多次调用会返回相同的过期时间值,并不会因为时间流逝而变化

    Deadline() (deadline time.Time, ok bool)

    // 返回一个channel,关闭该channel就代表关闭该Context。返回nil代表该Context不需要被关闭。

    // 多次调用返回会返回相同的值。

    Done() <-chan struct{}

    // 如果Context未关闭,则返回nil。

    // 否则如果正常关闭,则返回Canceled,过期关闭则返回DeadlineExceeded,

    // 发生错误则返回对应的error。

    // 多次调用返回相同的值。

    Err() error

    // 根据key从Context中获取一个value,如果没有关联的值则返回nil。

    // 其中key是可以比较的任何类型。

    // 多次调用返回相同的值。

    Value(key interface{}) interface{}

}

空的Context的实现

context包内部定义了许多Context接口的实现,其中最简单的就是emptyCtx了。

// emptyCtx 不需要关闭,没有任何键值对,也没有过期时间。

type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {

    return

}

func (*emptyCtx) Done() <-chan struct{} {

    return nil

}

func (*emptyCtx) Err() error {

    return nil

}

func (*emptyCtx) Value(key interface{}) interface{} {

    return nil

}

func (e *emptyCtx) String() string {

    switch e {

    case background:

        return "context.Background"

    case todo:

        return "context.TODO"

    }

    return "unknown empty Context"

}

context包中有两个emptyCtx的两个实例,

var (

    background = new(emptyCtx)

    todo       = new(emptyCtx)

)

分别通过context.Background()和context.TODO()获取。

Background():通常用作初始的Context、测试、最基层的根Context。

TODO():通常用作不清楚作用的Context,或者还未实现功能的场景。

WithValue

该函数的功能是,基于现有的一个Context,派生出一个新的Context,新的Context带有函数指定的key和value。内部的实现类型是valueCtx类型。

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

}

type valueCtx struct {

    Context

    key, val interface{}

}

func (c *valueCtx) String() string {

    return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)

}

func (c *valueCtx) Value(key interface{}) interface{} {

    if c.key == key {

        return c.val

    }

    return c.Context.Value(key)

}

从上面的源码可以看出,Context中的K/V存储并不是利用map实现的,而是先查询自身的一对键值,如果不匹配key,再向上层的Context查询。

官方对key的使用建议:

为了避免不同包的context使用冲突,不建议直接使用string和其他内建的类型作为key。而是自定义一个私有的key的类型,即小写开头的key类型。

并且最好定义一个类型安全的访问器(返回具体类型的函数,或其他方式)。

比如:

package user

import "context"

// 定义 User 类型,准备存储到Context中

type User struct {...}

// 定义了一个未导出的私有key类型,避免和其他包的key冲突。

type key int

// 该key实例是为了获取user.User而定义的,并且也是私有的。

// 用户使用user.NewContext和user.FromContext,而避免直接使用该变量

var userKey key = 0

// 返回一个新的Context,包含了u *User值

func NewContext(ctx context.Context, u *User) context.Context {

    return context.WithValue(ctx, userKey, u)

}

// FromContext returns the User value stored in ctx, if any.

// 从Context中获取*User值

func FromContext(ctx context.Context) (*User, bool) {

    u, ok := ctx.Value(userKey).(*User)

    return u, ok

}

为了避免分配内存,key通常也可以基于struct{}类型定义不同的类型

 type userKeyType struct{}

 context.WithValue(ctx, userKeyType{}, u)

注意,每一个struct{}类型的变量实际上它们的地址都是一样的,所以不会分配新的内存,但是重新定义后的不同struct{}类型,分别赋值给interface{}后,interface{}变量将不相等,比如:

type A struct{}

type B struct{}

a, b := A{}, B{}

var ia, ib, ia2 interface{}

ia, ib, ia2 = a, b, A{}

fmt.Printf("%p, %p, %v, %v", &a, &b, ia == ib, ia == ia2)

// 0x11b3dc0, 0x11b3dc0, false, true

使用WithValue的例子:

type favContextKey string

f := func(ctx context.Context, k favContextKey) {

    if v := ctx.Value(k); v != nil {

        fmt.Println("found value:", v)

        return

    }

    fmt.Println("key not found:", k)

}

k := favContextKey("language")

ctx := context.WithValue(context.Background(), k, "Go")

f(ctx, k)

f(ctx, favContextKey("color"))

// Output:

// found value: Go

// key not found: color

可关闭的Context

context包为可关闭的Context定义了一个接口:

type canceler interface {

    cancel(removeFromParent bool, err error)

    Done() <-chan struct{}

}

该接口有两个具体的实现,cancelCtx 和 timerCtx。

WithCancel

*cancelCtx由WithCancel返回,所以我们先看看WithCancel函数的定义:

// 从parent上派生出一个新的Context,并返回该和一个CancelFunc类型的函数

// 调用该cancel函数会关闭该Context,该Context对应的从Done()返回的只读channel也会被关闭。

// parent 对应的cancel函数如果被调用,parent派生的Context和对应的channel也都会被关闭。

// 当某项任务的操作完成时,应尽快关闭Context,以便回收Context关联的资源。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {

    // 创建一个cancelCtx类型的实例

    c := newCancelCtx(parent)

    // 关联该实例和父Context之间的关闭/取消关系,即关闭parent也关闭基于它派生的Context。

    propagateCancel(parent, &c)

    // 返回该实例和对应的关闭函数

    return &c, func() { c.cancel(true, Canceled) }

}

func newCancelCtx(parent Context) cancelCtx {

    return cancelCtx{Context: parent}

}

其中CancelFunc的定义也非常简单:

// 调用该函数意味着要关闭Context, 结束相关的任务。

// 第一次调用后,之后再次调用将什么都不做。

type CancelFunc func()

具体的propagateCancel实现和cancelCtx实现如下:

// 关联child和parent之间的关闭/取消关系,即关闭parent也关闭child。

func propagateCancel(parent Context, child canceler) {

    if parent.Done() == nil {

        return // parent 不需要关闭,则不需要关联关系

    }

    // 找到最近的cancelCtx类型的祖先Context实例

    if p, ok := parentCancelCtx(parent); ok {

        p.mu.Lock()

        if p.err != nil {

            // p 已经关闭,所以也关闭child

            child.cancel(false, p.err)

        } else {

            if p.children == nil {

                p.children = make(map[canceler]struct{})

            }

            p.children[child] = struct{}{}

        }

        p.mu.Unlock()

    } else {

        go func() {

            select {

            case <-parent.Done():

                child.cancel(false, parent.Err())

            case <-child.Done():

            }

        }()

    }

}

// 找到最近的cancelCtx类型或继承cancelCtx类型的祖先Context实例

func parentCancelCtx(parent Context) (*cancelCtx, bool) {

    for {

        switch c := parent.(type) {

        case *cancelCtx:

            return c, true

        case *timerCtx:

            return &c.cancelCtx, true

        case *valueCtx:

            parent = c.Context

        default:

            return nil, false

        }

    }

}

// 从最近的cancelCtx类型的祖先Context中移除child

func removeChild(parent Context, child canceler) {

    p, ok := parentCancelCtx(parent)

    if !ok {

        return

    }

    p.mu.Lock()

    if p.children != nil {

        delete(p.children, child)

    }

    p.mu.Unlock()

}

type canceler interface {

    cancel(removeFromParent bool, err error)

    Done() <-chan struct{}

}

// 代表已经关闭的channel

var closedchan = make(chan struct{})

func init() {

    close(closedchan)

}

// 实现可关闭的Context,关闭时,也将关闭它的子Context

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

}

func (c *cancelCtx) Done() <-chan struct{} {

    c.mu.Lock()

    if c.done == nil {

        c.done = make(chan struct{})

    }

    d := c.done

    c.mu.Unlock()

    return d

}

func (c *cancelCtx) Err() error {

    c.mu.Lock()

    defer c.mu.Unlock()

    return c.err

}

func (c *cancelCtx) String() string {

    return fmt.Sprintf("%v.WithCancel", c.Context)

}

// cancel closes c.done, cancels each of c's children, and, if

// removeFromParent is true, removes c from its parent's children.

func (c *cancelCtx) cancel(removeFromParent bool, err error) {

    if err == nil {

        panic("context: internal error: missing cancel error")

    }

    c.mu.Lock()

    if c.err != nil {

        c.mu.Unlock()

        return // already canceled

    }

    c.err = err

    if c.done == nil {

        c.done = closedchan

    } else {

        close(c.done)

    }

    for child := range c.children {

        // NOTE: acquiring the child's lock while holding parent's lock.

        child.cancel(false, err)

    }

    c.children = nil

    c.mu.Unlock()

    if removeFromParent {

        removeChild(c.Context, c)

    }

}

WithDeadline

理解了WithCancel,再理解WithDeadline并不难。

WithDeadline返回的是*timerCtx类型的Context,timerCtx继承 cancelCtx

// WithDeadline 根据parent和deadline返回一个派生的Context。

// 如果parent存在过期时间,且已过期,则返回一个语义上等同于parent的派生Context。

// 当到达过期时间、或者调用CancelFunc函数关闭、或者关闭parent会使该函数返回的派生Context关闭。

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {

    if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {

        // parent 已经过期,返回一个语义上等同于parent的派生Context

        return WithCancel(parent)

    }

    c := &timerCtx{

        cancelCtx: newCancelCtx(parent),

        deadline:  deadline,

    }

    propagateCancel(parent, c)

    d := time.Until(deadline)

    if d <= 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(d, func() {

            c.cancel(true, DeadlineExceeded)

        })

    }

    return c, func() { c.cancel(true, Canceled) }

}

// timerCtx继承 cancelCtx,并且定义了过期时间。

// 当关闭 timerCtx时会关闭timer和cancelCtx

type timerCtx struct {

    cancelCtx

    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time

}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {

    return c.deadline, true

}

func (c *timerCtx) String() string {

    return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, time.Until(c.deadline))

}

func (c *timerCtx) cancel(removeFromParent bool, err error) {

    c.cancelCtx.cancel(false, err)

    if removeFromParent {

        // 从父Context的children字段中移除自己

        removeChild(c.cancelCtx.Context, c)

    }

    c.mu.Lock()

    if c.timer != nil {

        c.timer.Stop()

        c.timer = nil

    }

    c.mu.Unlock()

}

WithTimeout

与WithDeadline稍由不同,WithTimeout传递的是一个超时时间间隔,而WithDeadline传递的是一个具体的过期时间。一般情况下WithTimeout比WithDeadline更常用:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {

    return WithDeadline(parent, time.Now().Add(timeout))

}

例子:

func slowOperationWithTimeout(ctx context.Context) (Result, error) {

    ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)

    defer cancel()  // 如果slowOperation在超时之前完成,则需要调用cancel关闭Context,以便回收Context相关联的资源

    return slowOperation(ctx)

}

使用例子

最后,我们再举几个例子来加深印象。

WithCancel的例子

如何使用Context让goroutine退出,避免goroutine内存泄漏。

// 在独立的goroutine中生成整数,通过channel传递出去。

// 一旦context关闭,该goroutine也将安全退出。

gen := func(ctx context.Context) <-chan int {

    dst := make(chan int)

    n := 1

    go func() {

        for {

            select {

            case <-ctx.Done():

                return // returning not to leak the goroutine

            case dst <- n:

                n++

            }

        }

    }()

    return dst

}

ctx, cancel := context.WithCancel(context.Background())

defer cancel() // 当完成整数生产时,关闭Context

for n := range gen(ctx) {

    fmt.Println(n)

    if n == 5 {

        break

    }

}

// Output:

// 1

// 2

// 3

// 4

// 5

WithDeadline的例子

当某项任务需要在规定的时间内完成,如果未完成则需要立即取消任务,并且返回错误的情况,可以使用WithDeadline。

    d := time.Now().Add(50 * time.Millisecond)

    ctx, cancel := context.WithDeadline(context.Background(), d)

    // 即使ctx会因为过期而关闭,我们也应该在最后调用cancel,因为任务可能会在规定时间内完成,这种情况需要主动调用cancel来尽快释放Context资源

    defer cancel()

    select {

    case <-time.After(1 * time.Second):

        fmt.Println("overslept")

    case <-ctx.Done():

        fmt.Println(ctx.Err())

    }

    // Output:

    // context deadline exceeded

WithTimeout例子

同WithDeadline,只是传递的是一个time.Duration

ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)

    defer cancel()

    select {

    case <-time.After(1 * time.Second):

        fmt.Println("overslept")

    case <-ctx.Done():

        fmt.Println(ctx.Err()) // prints "context deadline exceeded"

    }

    // Output:

    // context deadline exceeded

WithValue的例子

WithValue在前面的介绍中已经举过一个例子,我们再举一个http.Request相关的Context的例子。

在 Golang1.7 中,"net/http"原生支持将Context嵌入到 *http.Request中,并且提供了http.Request.Conext() 和 http.Request.WithContext(context.Context)这两个函数。

http.Request.Conext()函数返回或新建一个 context。

http.Request.WithContext(context.Context)函数返回一个新的Request,并且将传入的context与新的Reuest实例关联。

type userKey string

const userIDKey userKey = "uid"

func requestFilter(next http.Handler) http.Handler {

    return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {

        cook, err := req.Cookie("USERID")

        uid := ""

        if err == nil {

            uid = cook.Value

        }

        ctx := context.WithValue(req.Context(), userIDKey, uid)

        next.ServeHTTP(w, req.WithContext(ctx))

    })

}

func userIdFromContext(ctx context.Context) string {

    return ctx.Value(userIDKey).(string)

}

func process(w http.ResponseWriter, req *http.Request) {

    uid := userIdFromContext(req.Context())

    fmt.Fprintln(w, "user ID is ", uid)

    return

}

func main() {

    http.Handle("/", requestFilter(http.HandlerFunc(process)))

    http.ListenAndServe(":8080", nil)

}

结束

本文详解了context包源码的实现,并结合了一些例子来说明用法。相信读者对context包已经有一定的理解了,还可以再看看源码包中完整的代码,来加深印象哦。

©著作权归作者所有:来自51CTO博客作者recallsong的原创作品,如需转载,请注明出处,否则将追究法律责任


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消