天天看點

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/

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利。