注:写帖子时go的版本是1.12.7
Context的github地址
go
语言中实现一个
interface
不用像其他语言一样需要显示的声明实现接口。
go
语言只要实现了某
interface
的方法就可以做类型转换。
go
语言没有继承的概念,只有
Embedding
的概念。想深入学习这些用法,阅读源码是最好的方式.
Context
的源码非常推荐阅读,从中可以领悟出
go
语言接口设计的精髓。
对外暴露Context接口
Context
源码中只对外显露出一个
Context
接口
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
对于
Context
的实现源码里有一个最基本的实现,就是私有的
emptyCtx
,他也就是我们经常使用的
context.Background()
底层的实现,他是一个
int
类型,实现了
Context
接口的所有方法,但都是没有做任何处理,都是返回的默认空值。只有
String()
方法,里有几行代码,去判断
emptyCtx
的类型来进行相应的字符串输出,
String()
方法其实是实现了接口
Stringer
。
emptyCtx
是整个
Context
的
灵魂
,为什么这么说,因为你对
context
的所有的操作都是基于他去做的再次封装。
注意一下
Value(key interface{}) interface{}
,因为还没有
泛型
,所以能用的做法就是传递或者返回
interface{}
。不知道
Go2
会不会加入
泛型
,说是会加入,但是还没有出最终版,一切都是未知的,因为前一段时间还说会加入
try
,后来又宣布放弃。
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"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
在使用
Context
时我们能直接得到就是
background
和
todo
func Background() Context {
return background
}
func TODO() Context {
return todo
}
其他所有对外公开的方法都必须传入一个
Context
做为
parent
,这里设计的很巧妙,为什么要有
parent
后面我会详细说。
可以cancel掉的Context
可以cancel掉的context有三个公开的方法,也就是,是否带过期时间的
Context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
Context
只用关心自己是否
Done()
,具体这个是怎么完成的他并不关心,是否可以
cancel
掉也不是他的业务,所以源码中把这部分功能分开来。
Context
最常用的功能就是去监控他的
Done()
是否已完成,然后判断完成的原因,根据自己的业务展开相应的操作。要提一下
Context
是线程安全的,他在必要的地方都加了锁处理。
Done()
的原理:其实是
close
掉了
channel
所以所有监控
Done()
方法都能知道这个
Context
执行完了。
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
v, err := DoSomething(ctx)
if err != nil {
return err
}
select {
case <-ctx.Done():
return ctx.Err()
case out <- v:
}
我这里不缀述
Context
是如何使用的。这篇帖子主要分析的是源码。
Context
可以被
cancel
掉需要考虑几个问题:
- 如何处理父或子
Context
cancel
-
后cancel
是否也应该删除掉。Context
我们从源码中来找到答案。
看一下
canceler
的接口,这是一个独立的私有接口,和
Context
接口独立开来,
Context
只做自己的事,并不用关心自己有啥附加的功能,比如现在说的
cancel
功能,这也是一个很好的例子,如果有需要对
Context
进行扩展,可以参考他们的代码。
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
和两个错误
var Canceled = errors.New("context canceled")
var DeadlineExceeded error = deadlineExceededError{}
是个是被主动
Cancel
的错误和一个
超时
的错误,这两个错误是对外显露的,我们也是根据这两个
Error
判断
Done()
是如何完成的。
实现
canceler
接口的是结构体
cancelCtx
// that implement canceler.
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
}
注意:把
cancelCtx
Context
进去了,也就是说
Embedding
多重实现接口,不但是个
cancelCtx
类型也是一个
canceler
Context
类型。
源码中
并没有实现
cancelCtx
接口中的所有的方法,这就是
Context
的强大之处,
Embedding
接口的具体实现都是外部传进来的具体
Context
实现类型来实现的eg:
Context
还要注意一点就是这两个接口都有各自的
cancelCtx{Context: xxxx}
方法,
Done()
有实现自己的
cancelCtx
方法,也就是说无论转换成
Done()
接口类型还是
canceler
类型调用
Context
方法时,都是他自己的实现
Done()
以
cancelCtx
为基础还有一个是带过期时间的实现
timerCtx
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
timerCtx
是
WithDeadline
WithTimeout
方法的基础。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithCancel
需要调用者主动去调用
cancel
,其他的两个,就是有过期时间,如果不主动去调用
cancel
到了过期时间系统会自动调用。
上面我有说过包中
context
Background()
方法,是其他所有公开方法的基础,因为其他所有的公开方法都需要传递进来一个
TODO()
接口做为
Context
。这样我们所有创建的新的
parent
都是以
Context
为基础来进行封装和操作
parent
cancelCtx
的是如何初始化的
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
propagateCancel
回答了我们第一个问题
Context
cancel
func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {
return // parent is never canceled
}
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{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
propagateCancel
做了以下几件事
- 检查
是否可以parent
cancel
-
是否是parent
cancelCtx
类型
2.1. 如果是,再检查是否已经
掉,是则cancel掉cancel
,否则加入child
2.2. 如果不是,则监控child
parent
child 的Done()
我们看一下
timerCtx
的具体实现
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)
}
}
我们去查看所有对
cancel
的调用会发现
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
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) }
}
返回的
cancel
方法都是
func() { c.cancel(true, Canceled) }
回答了我们的第二个问题
cancel
所有创建的可以
Context
掉的方法都会被从
cancel
上删除掉
parent
保存key/value信息的Context
Context
还有一个功能就是保存
key/value
的信息,从源码中我们可以看出一个
Context
只能保存一对,但是我们可以调用多次
WithValue
创建多个
Context
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}
}
在查询
key
的时候,是一个向上递归的过程:
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
总结一下
- 接口要有边界,要简洁。
- 对外公开的部分要简单明了。
- 提炼边界方法和辅助实现部分,隐藏细节。
作者:李鹏
出处:http://www.cnblogs.com/li-peng/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。