天天看点

golang从context源码领悟接口的设计

注:写帖子时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

接口的具体实现都是外部传进来的具体

Context

实现类型来实现的eg:

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

做了以下几件事

  1. 检查

    parent

    是否可以

    cancel

  2. parent

    是否是

    cancelCtx

    类型

    2.1. 如果是,再检查是否已经

    cancel

    掉,是则cancel掉

    child

    ,否则加入

    child

    2.2. 如果不是,则监控

    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/

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。