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