context是Go语言提供的用于控制协程的重要工具包,提供了许多开发中非常实用的功能,下面就对各种各样的context进行分析,并结合案例说明应用场景。
1. Context概述
Context其实只是一个接口,凡是实现这个接口的都可以称之为context。该接口包含四个方法,官方对每个方法的功能和职责都做了非常详细的介绍。
1.Deadline() (deadline time.Time, ok bool)
//该函数返回代表当前context完成工作后应该被cancel的截止时间,
//如果没有设置截止时间,ok会等于false,函数多次调用都会返回相同的结果
Deadline() (deadline time.Time, ok bool)
2.Done() <-chan struct{}
// 返回一个channel,当代表当前context完成工作后应该被cancel时,channel会被关闭。如果一个上下文
// 不能被cancel,Done就会返回nil,连续调用会返回相同结果。done函数返回的channel的关闭
// 可能会在cancel函数返回后异步执行。Done函数通常依赖select函数实现需求。
//这里可以看到更多关于Done函数cancel的案例: https://blog.golang.org/pipelines
Done() <-chan struct{}
使用案例
//Stream函数调用DoSomething函数生成values,
//然后把这些values发给名称为out的channel,
//直到context返回一个错误或者ctx.Done的channel被关闭。
func Stream(ctx context.Context, out chan<- Value) error {
for {
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
}
}
- Err() error
//如果Done返回的channel没有被关闭,Err函数返回nil,
//如果关闭了,Err会返回一个非nil的error解释原因:
//如果context是cancel了,error就是Canceled
//如果是截止时间到了,error就是DeadlineExceeded
//在Err返回一个非nil的error后,连续调用会得到相同的结果。
Err() error
4.Value(key interface{}) interface{}
//Value函数返回一个和当前context关联的key的value,如果没有value和key关联返回nil
//连续调用Value方法如果key相同,返回结果也相同。
//context的value仅用于请求数据的传输流程和api边界,不是用来给函数传递可选字段的。
//一个key代表了在一个context的指定的value,对于希望存储values在context里面的函数,
//通常是声明一个全局变量做key,然后把这个key作为参数,调用context.WithValue()、
//context.Value(),分别实现存值和取值。key的类型可以是任意支持相等比较任意类型
//使用者的包定义的这些key应该是非导出类型从而避免key冲突。
//使用者的包定义一个context的key应该提供一个类型安全的获取函数来说访问存储的value
Value(key interface{}) interface{}
2.Context类型
通过上面接口职责说明里面可以看到,不是所有的context都需要完全实现上面的方法,为了实现接口而去实现不需要的方法就会很多余,go语言通过一个empty类型的context解决了这个问题,下面会具体分析。
- emptyCtx
-
使用:调用
context.Background()
方法来生成。(内部直接返回包内的全局变量emptyCtx类型的background) -
作用:代表一个空的context。通常用来作为生成其他类型的context的父context。
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"
}
2.cancelCtx
- 使用: 调用
context.WithCancel(context.Background())
格式 生成。 - 作用:是一个可以执行cancel的上下文,通过消息对协程进行统一控制。
//初始化一个cancelCtx实例
//如果父context也支持cancel,添加到父节点上。
// 返回cancelCtx实例和cancel方法。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
添加到父parent函数的实现
func propagateCancel(parent Context, child canceler) {
done := parent.Done() //
if done == nil {
return //说明parent不支持cancel,直接返回吧
}
select {
case <-done:
// parent已经cancel了,当前也执行cancel,并传递父parent的err
child.cancel(false, parent.Err())
return
default:
}
// 查找支持parent的父节点
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
//如果父parent支持cancel
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else { // 如果parent的的父节点都不支持cancel
atomic.AddInt32(&goroutines, +1)// 当前包内的协程数量开启数+1
go func() {
select {
case <-parent.Done(): // 等待parent节点结束
child.cancel(false, parent.Err()) // 在把当前的结束。
case <-child.Done():
}
}()
}
}
parentCancelCtx函数实现。
// 返回父级的中最基础的*cancelCtx,通过调用parent.Value(&cancelCtxKey)
// 寻找最里面的*cancelCtx,然后检查parent.Done()是否和*cancelCtx匹配,
// 如果不匹配,说明*cancelCtx被一个自定义实现包装过了,提供了一个不同的done
// channel,在这种情况下我们不应该绕过它。
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
p.mu.Lock()
ok = p.done == done
p.mu.Unlock()
if !ok {
return nil, false
}
return p, true
}
// 这是一个可以被取消的context,当调用Cancel的时候,所有的实现cancler接口的子context也会调用cancel
type cancelCtx struct {
// 这里是一个接口,蕴含的形式定义的,通常我们可以用emptyCtx来往这里放,
// emptyCtx完整实现了context,所以凡是这样写的都可以按重写需要的接口方法,比如
// 这个就没有重写deadLine方法,但是依然时context,其他类型的context都采用了类似形式。
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) Value(key interface{}) interface{} {
if key == &cancelCtxKey { // 这个key专门返回自己
return c
}
return c.Context.Value(key) // 其他的调用c.Context.Value()方法实现。
}
// c.done可能是nil,生命周期nil,chan,close
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()
err := c.err
c.mu.Unlock()
return err
}
cancelCtx还实现了canceler
,这接口代表了可以cancel的context应该实现的方法。
type canceler interface {
// 取消函数(是否从父节点移出,err是取消原因,不能为nil)
cancel(removeFromParent bool, err error) //
// 完成以后发的消息。
Done() <-chan struct{}
}
对cancel方法的实现
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)
}
}
使用案例
var ctx, cancel = context.WithCancel(context.Background())
for i := 0; i < 2; i++ {
go func(ctx context.Context, i int) {
go func(ctx2 context.Context,i2 int) {
for ; ; {
select {
case <-ctx.Done():
err := ctx.Err()
fmt.Printf("i2_1 function done value %v err %s\n", i2,err.Error())
return
default:
fmt.Println("i2_1 doing")
//time.Sleep(5*time.Microsecond)
}
}
}(ctx,i)
go func(ctx2 context.Context,i2 int) {
for ; ; {
select {
case <-ctx.Done():
err := ctx.Err()
fmt.Printf("i2_2 function done value %v err %s\n", i2,err.Error())
return
default:
fmt.Println("i2_2 doing")
//time.Sleep(5*time.Microsecond)
}
}
}(ctx,i)
}(ctx,i)
}
time.Sleep(100*time.Microsecond) // 模拟逻辑执行
cancel() // 取消以后,创建的协程会收到取消通知
time.Sleep(1000*time.Microsecond) // 模拟后续执行流程
创建了多个协程,包括嵌套创建的。然后在协程里面执行逻辑,并监听ctx.Done的消息。通过调用cancel方法每个协程都收到ctx的cancel消息了。
输出结果如下
i2_2 doing
i2_2 doing
i2_1 doing
i2_2 doing
i2_2 function done value 1 err context canceled
i2_1 function done value 0 err context canceled
i2_2 function done value 0 err context canceled
i2_1 function done value 1 err context canceled
- timerCtx
- 使用:调用
WithDeadline
或者WithTimeout
方法 - 作用:timerCtx基于cancelCtx实现,增加了deadline用于标记自动cancel截止时间,timer就是对时间的计时器。
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
//WithDeadline返回带有一个调整了不迟于d的截止日期的父context的拷贝,如果父context截止日期
// 已经早于d,等同于返回的是parent,返回的上下文里面的Done channel会在截止时间过期、
// 或者返回的cancel函数被手动执行,或者parent的context的Done channel被关闭时关闭。
// 不论哪一个情况第一次发生都会关闭。
// canceling一个context会释放和它关联的资源,所以代码应该在完成在context的操作后立即调用。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// 当前设置的时间比parent的早,用parent的
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) }
}
// WithTimeout 返回的结果是 WithDeadline(parent, time.Now().Add(timeout)).
//
// 取消context操作将会释放和它关联的资源, 所以代码里面应该尽可能早的在上线文中调用cancel:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() //如果slowOperation在超时之前完成释放资源。
// return slowOperation(ctx)
// }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
接口实现
// 直接返回截止时间
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
// 和cancelCtx差不多,只是关闭了定时器。
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
if removeFromParent {
// Remove this timerCtx from its parent cancelCtx's children.
removeChild(c.cancelCtx.Context, c)
}
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
4.valueCtx
- 使用:
context.WithValue()
- 作用:各个协程之间传递一些数据, 只需要实现context的value接口。
// WithValue returns a copy of parent in which the value associated with key is
// val.
// 设置值
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
// key是可以用来相等比较
if !reflectlite.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
}
// 会从父context去查询。
return c.Context.Value(key)
}
使用案例,下面的案例来着官方
// user包定义了一个User类型存储在context。
package user
import "context"
// User 是存储在context里面的值。
type User struct {...}
// key定义了非导出的数据类型,仅包内可见,
// 这避免了和其他包的冲突
type key int
// userKey是代表在context里面的user.User作为value时对应的key,也是非导出的,
// 使用者应该使用 ser.NewContext和user.FromContext函数,而不是直接使用这个key
var userKey key
// 返回一个新的context并且设置一个value u.
func NewContext(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}
// 获取存储在context里面的User类型的value。
func FromContext(ctx context.Context) (*User, bool) {
u, ok := ctx.Value(userKey).(*User)
return u, ok
}
总结:
- Context仅仅是一个接口定义,跟据实现的不同,可以衍生出不同的context类型;
- cancelCtx实现了Context接口,通过WithCancel()创建cancelCtx实例;
- timerCtx实现了Context接口,通过WithDeadline()和WithTimeout()创建timerCtx实例;
- valueCtx实现了Context接口,通过WithValue()创建valueCtx实例; - - 三种context实例可互为父节点,从而可以组合成不同的应用形式;
网友评论