六游的博客小站
Context初识、源码解析以及最佳实践
发布于: 2019-06-13 更新于: 2019-06-14 阅读次数: 

本篇文章将从Golang Context包的一些基本使用场景开始,逐步深入,从源码的角度来介绍一下Context的实现原理。最后会给出一些在使用Context时候的一些建议

初识Context

Context是Go1.7之后才出现的一个标准库,Context诞生的主要目的是为了协调多个Goroutine工作,这些协调工作包括:通信(传递消息)、同步、退出

使用Context实现通知退出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func main() {
//为我们生成一个用于关闭的context以及关闭的方法
ctx, cancel := context.WithCancel(context.Background())
//将生成的context传入下一个groutine中
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("监控退出,停止了...")
return
default:
fmt.Println("goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}(ctx)

time.Sleep(10 * time.Second)
fmt.Println("可以了,通知监控停止")
cancel()
//为了检测监控是否停止,如果没有监控输出,就表示停止了
time.Sleep(2 * time.Second)

}

使用context实现超时控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func main() {
ctx,cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

go read(ctx)
}
//Read方法从一个Channel中读取数据,如果读到了数据,就返回,如果在context规定的时间内没有读到,会直接返回空字符串
func read(ctx context.Context) string {
select{
case <-ctx.Done():
return ""
case a<-dataChannel:
return a
}
}

使用context实现goroutine之间的传值

1
2
3
4
5
6
7
8
9
10
11
12
13
type myContextKey string
var key myContextKey = "myKey"

func main() {
ctx := context.WithValue(context.Background(), key, "我是要传入其他goroutine的值")
go handle(ctx)
}
func handle(ctx context.Context){
v := ctx.Value(key)
if v != nil {
fmt.println()
}
}

Context原理解析

工作机制

由于各个Goroutine之间并没有显式的联系,对于Go来说每个Goroutine都是平等的,并没有父与子的概念,因此当遇到一些复杂的业务的时候,就难以处理,所以Go推出了Context机制来实现对Goroutine的管理。Context采用树结构来管理,第一个被创建的context为树的根节点,可以根据根节点派生出其他的实现context接口的对象,并将其向下传递至新创建的Goroutine,下游Goroutine可以继续派生并向下传递。就这样由一串实现了Context接口的对象构成的树将许多个Goroutine联系了起来。

Context核心接口与结构体

Context接口:是context中最为核心的接口,所有的context对象都要实现这个接口

1
2
3
4
5
6
7
8
9
10
type Context interface {
//如果定义了超时控制,则返回的deadline为超时时间,ok为true。否则ok为false
Deadline() (deadline time.Time, ok bool)
//返回一个只读的channel,用于通知退出
Done() <-chan struct{}
//返回一个error,来说明退出通知发出的原因
Err() error
//根据特定的key来取得特定的值
Value(key interface{}) interface{}
}

canceler接口:是具有通知关闭功能的context需要实现的一个拓展接口

1
2
3
4
5
type canceler interface {
//用来向自己的context发送关闭通知,以及递归关闭自己的其他子context,removeFromParent标识关闭通知是否来源于自己的父context节点,err说明要发送关闭通知的原因
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}

emptyCtx:该类型以最小程度实现了Context接口,是一个不具备任何功能,并且也不能被关闭的context,该类型存在的目的是作为context对象树的根节点。在Context包中,Golang已经为我们准备了两个emptyCtx的对象,分别是background与todo,我们可以调用方法Background()与TODO()获取到这两个对象,我们一般使用Background()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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对象
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)

func Background() Context {
return background
}
func TODO() Context {
return todo
}

Context的衍生

1
2
3
4
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

以上的这四个方法用来从一个父context对象衍生出子context对象,我们可以看到前三个方法除了返回了衍生的context之外,还返回了一个CancelFunc方法,其实前三个方法返回的context对象,都实现了cancler接口,接下来我们来具体看一下返回的这个cancelFunc方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//Golang中将CancelFunc定义为一个空参的方法
type CancelFunc func()

//以上衍生方法返回的CancelFunc实际上是由cancel加上参数以及调用对象构成,cancel方法用来关闭自身context,并递归关闭所有自己的所有子context
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
}
c.err = err
if c.done == nil {
//说明已经关闭
c.done = closedchan
} else {
close(c.done)
}
//递归关闭子context
for child := range c.children {
child.cancel(false, err)
}
c.children = nil
c.mu.Unlock()
//如果removeFromParent为true,会获取该context的父节点,并将自己从父节点的字节点列表中删除
if removeFromParent {
removeChild(c.Context, c)
}
}

各个Contex衍生结构的详细分析

cancelCtx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
type cancelCtx struct {
//继承Context接口
Context
//锁,保证并发安全
mu sync.Mutex
//用来通知退出的channel
done chan struct{}
//用来存储子context,如果自身的context关闭则需要通知自己的所有子context也关闭
children map[canceler]struct{}
//error功能跟之前一样,不再赘述
err error
}
//根据传入的父节点,派生出cancelContext
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
//此方法的作用为,判断parent是否为cancelCtx或者timerCtx,如果是,就将新创建的context加到父context的字节点列表中去,否则就开始监听父context的Done(),如果监听到父context退出,则执行本context的退出方法
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}

timerCtx

WithTimeOut()与WithDeadLine()返回的都是timerCtx的实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
type timerCtx struct {
//继承了cancelCtx这个struct
cancelCtx
//定时器,用于定时执行关闭context的操作
timer *time.Timer
//该context到期的时间
deadline time.Time
}

//其中timerCtx相关的核心方法是WithDeadLine(),下面来看一下这个方法做了什么
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
//首先判断父context是否拥有DeadLine,并且已经到时,如果是,就没有必要继续创建,直接返回一个cancelCtx
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
//创建timerCtx对象
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
//这个方法的作用在上一个cancelCtx的说明中提到过,不再赘述
propagateCancel(parent, c)
//根据传入的时间(DeadLine)获取从现在开始到到时时间一共还有多长时间,如果duration小于0,说明已经过期,将context关闭后返回
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded)
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
//如果没有错误,就开启一个计时器,到期后执行context的退出方法
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}

//搞懂了WithDeadline方法,WithTimeout方法也就知道了,因为WithTimeout方法内部也是直接调用的WithDeadline方法
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}

valueCtx

有关于valueCtx的代码非常的简单,主要就有下面两个核心方法,方法内部具体做了什么一看就懂了,就不再赘述了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type valueCtx struct {
Context
key, val interface{}
}

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

func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}

关于使用Context时的一些建议与最佳实践

  • Context要使用参数传递,不要把它作为一个属性放入结构体中
  • 以Context作为参数的方法,应该把Context作为第一个参数
  • 不要给一个Context参数传递nil,当你不需要Context但这个方法的参数里有Context的时候,就传Context.TODO()
  • Context的Value必须传递必须的值,不要什么值都用Value来传递。Value应该用来传递一些请求范围值(比如说鉴权生成的用户ID、请求ID、用户IP),而不能是全局的(比如数据库连接)。每当你使用Context的Value的时候,你都应该告诉自己Context.Value要做的事情是通知而不是控制。最后你应该尝试尽量不要去用Value
  • 所有可能阻塞或者长时间的操作都应该是可取消的
--- 本文结束 The End ---