深入理解 golang 中的 context

在golang中goroutine是没有主从关系的,是平等的,也就是说goroutineA启动了goroutineB,即使goroutineA结束了,对goroutineB不会有影响,当然如果主程序结束了,所有的goroutine都会结束;在goalng作为服务运行时,各个goroutine的生命周期则需要人为来控制,否则容易出现goroutine泄露的情况。

Golang 的 context Package 提供了一种简洁又强大方式来管理 goroutine 的生命周期,同时提供了一种 Requst-Scope K-V Store。但是对于新手来说,Context 的概念不算非常的直观,这篇文章来带领大家了解一下 Context 包的基本作用和使用方法。

应用场景:在 Go http 包的 Server 中,每一个请求在都有一个对应的goroutine去处理。请求处理函数通常会启动额外的goroutine用来访问后端服务,比如数据库和 RPC 服务。用来处理一个请求的goroutine通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的 token、请求的截止时间。当一个请求被取消或超时时,所有用来处理该请求的goroutine都应该迅速退出,然后系统才能释放这些goroutine占用的资源。

1. 包的引入

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

通道ch被close(ch)之后,ch的读端会收到消息
p, ok := <-ch
p的值是ch类型的零值
ok为false

2、基本数据结构
2-1 Context interface

它是最基本的接口

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
Deadline():返回一个time.Time,是当前 Context 的应该结束的时间,ok 表示是否有 deadline
Done():返回一个struct{}类型的只读 channel
Err():返回 Context 被取消时的错误
Value(key interface{}):是 Context 自带的 K-V 存储功能

为什么 Done() 方法要返回只读的通道呢?
这是出于安全考虑,防止此通道被写入数据从而导致协程退出,设置成只读的话,那就只有 close 这一个操作可以触发通道行为了。

2-2 canceler interface

定义了提供 cancel 函数的 context,当然要求数据结构要同时实现 Context interface

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

emptyCtx是空的Context,只实现了Context interface,只能作为 root context 使用。

type emptyCtx int
cancelCtx

cancelCtx 继承了 Context 并实现了canceler interface,从WithCancel()函数产生

type cancelCtx struct {
    Context

    done chan struct{} // closed by the first cancel call.

    mu       sync.Mutex
    children map[canceler]bool // set to nil by the first cancel call
    err      error             // set to non-nil by the first cancel call
}

cancelCtx 中的 Context 元素为 Context 类型,也就是实现了 Context 接口的变量。
因此cancelCtx也实现了Context。

为什么 cancelctx 还要重新实现 Done 方法呢?
假如传入一个 emptyCtx 作为基础ctx 得到 cancelctx,此时 cancelctx 自然实现了 Context 和 canceler 的 Done 方法,但是这是继承自 emptyCtx 的实现,是这样的:

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

要知道 nil chan 是无法被使用的,否则 panic ,因此需要重新实现

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
}

这里有一个细节,虽然 Done() 方法只需要返回可读通道,但是必须要 make 一个双向的通道,因为最终 close 一个只读的通道是会报错的,也就是说 c.done 必须为双向通道。

这也是 Done() 方法存在的意义,别看它只是简单的返回一个对象。

timerCtx

timerCtx继承了cancelCtx,所以也自然实现了Context和canceler这两个interface,由WithDeadline()函数产生

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

	deadline time.Time
}
valueCtx

valueCtx包含key、val field,可以储存一对键值对,由WithValue()函数产生

type valueCtx struct {
    Context
    key, val interface{}
}
Context 实例化和派生
Context 实例化

Context 只定义了 interface,真正使用时需要实例化,官方首先定义了一个 emptyCtx struct 来实现 Context interface,然后提供了Backgroud() 和 TODO() 函数来便利的生成一个 emptyCtx 实例。

type emptyCtx int

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

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

Backgroud() 生成的 emptyCtx 实例是不能取消的,因为emptyCtx没有实现canceler interface,要正常取消功能的话,还需要对 emptyCtx 实例进行派生。常见的两种派生用法是WithCancel(),WithTimeout,WithDeadline。

emptyCtx 没有提供 cancel的 能力。
cancelCtx 包含了emptyCtx,并提供 cancel 的能力。
timerCtx 包含了 cancelCtx,提供了超时后自动调用 cancel 的能力,当然也可以手动 cancel 。 
WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}
WithDeadline
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(false, 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) }
}

传递一个截止的时间。

timerCtx 有一个 timer 属性,该属性对应一个定时器,

c.timer = time.AfterFunc(dur, func() {
	c.cancel(true, DeadlineExceeded)
})

初始化一个定时器,到时自动执行 cancel 操作。

即便如此,出于安全考虑,我们还是应该在主代码手动执行 defer cancel()

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

传递一个超时时间time.Duration,和 WithDeadline 用途一样。

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}
}
context 包的使用

context 包可以用来控制协程的退出,协程间共享数据。

一般是这样使用的

go func() {
	for {
		select {
		case <-ctx.Done():
			return
		case xxx:
			yyy
		}
	}
}()

示例:

func main() {
	C6()
}

func C6() {
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	go func(ctx context.Context) {
		for  {
			select {
			case <-ctx.Done():
				fmt.Println("ctx done")
				return
			default:
				// do some thing
			}
		}
	}(ctx)

	rand.Seed(time.Now().UnixNano())

	for  {
		n := rand.Intn(6)
		fmt.Println(n)
		if n == 5 {
			return
		}
		time.Sleep(1*time.Second)
	}
}

程序结束了也没打印 ctx done ,为什么呢?
那时因为主程序结束了,一切都结束了。修改 main 方法:

func main() {
	go C6()
	
	for  {
		time.Sleep(1*time.Second)
	}
}

这样就有了。

或许,你就要问了,就这,非得用context吗,我使用通道也能解决啊,因为关闭一个通道,所有的读端也都会收到消息。

func C7() {
	clo := make(chan struct{})
	defer close(clo)

	for i := 0; i < 3; i++ {
		// readonly chan
		go func(clo <-chan struct{}) {
			for {
				select {
				case <-clo:
					fmt.Println("ctx done")
					return
				default:
					// do some thing
				}
			}
		}(clo)
	}

	rand.Seed(time.Now().UnixNano())

	for {
		n := rand.Intn(6)
		fmt.Println(n)
		if n == 5 {
			return
		}
		time.Sleep(1 * time.Second)
	}
}

一样的效果,那么context包的必要性在哪里呢?

一个超时的例子

func C2() {
	d := time.Now().Add(50 * time.Millisecond)
	ctx, cancel := context.WithDeadline(context.Background(), d)

	defer cancel()

	select {
	case <-time.After(1 * time.Second):
		fmt.Println("overslept")
	case <-ctx.Done():
		fmt.Println(ctx.Err())
	}
}

一个WithValue的例子

func C3() {
	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"))
}

context 就是一个工具,有助于我们更方便的实现以上这些功能,使程序显得优雅简洁,但并不是必须的。

你可能感兴趣的:(golang)