书接上回,我们在外部通过通道控制goroutine,但是在跨包时调用,依然存在不容易实现规范和统一,还需要维护一个共用的channel;基于此,go标准包为我们提供了context包。

1.开门见山

package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	wg.Add(1)
	ctx, cancel := context.WithCancel(context.Background())
	go func(ctx context.Context) {
	LOOP:
		for {
			fmt.Println("goroutine...")
			time.Sleep(time.Second)
			select {
			case <-ctx.Done(): // 等待上级通知
				break LOOP
			default:
			}
		}
		wg.Done()
	}(ctx)
	time.Sleep(time.Second * 3) 
	cancel()
	wg.Wait()
	fmt.Println("exit")
}

标准库context是Go1.7加入了,它定义了Context类型,专门用来简化对于多个 goroutine 之间协调的取消信号、截止时间等相关操作。

2.context的使用

2.1 Background()和TODO()

context包内置了Background()TODO()函数。代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象。

Background()主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。

TODO(),就是当开发者还不知道接下来的context具体的使用场景,就可以使用这个,像个占位符一样。

backgroundtodo本质上都是 emptyCtx自定义类型 ,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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
}
var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
	return background
}

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
	return todo
}

2.2 With函数

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) 
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) 
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

函数都接收一个Context类型的参数parent,并返回一个Context类型的值,这样就层层创建出不同的节点。子节点是从复制父节点得到的,并且根据接收参数设定子节点的一些状态值,接着就可以将子节点传递给下层的Goroutine了。

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 timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}
type valueCtx struct {
	Context
	key, val interface{}
}

其他不赘述,在使用中感受吧。

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

WithCancel为例,当主动调动返回的CancelFunc函数:

c.mu.Lock()
//omit...
close(c.done)
//omit...
c.mu.Unlock()

此时的Done()返回的通道刚好就能接收到值<-ctx.Done()

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
}

4.使用Context的注意事项

  • 以参数的方式显示传递Context
  • 以Context作为参数的函数方法,应该把Context作为第一个参数。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
  • Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
  • Context是线程安全的,可以放心的在多个goroutine中传递