使用Go
作為服務(wù)端開發(fā)時(shí)漓踢,每個(gè)請(qǐng)求過來都會(huì)分配一個(gè)goroutine
來處理,請(qǐng)求處理過程中漏隐,可能還會(huì)創(chuàng)建額外的goroutine
訪問DB或者RPC服務(wù)喧半。這個(gè)請(qǐng)求涉及的goroutine
可能需要訪問一些特定的值比如認(rèn)證token、用戶標(biāo)識(shí)或者請(qǐng)求截止時(shí)間青责。當(dāng)一個(gè)請(qǐng)求取消或者超時(shí)挺据,則這個(gè)請(qǐng)求涉及的goroutine
也應(yīng)該被終止,這樣系統(tǒng)就可以快速回收這部分資源脖隶。
基于簡化目的扁耐,context
包定義了Context
類型,來傳遞超時(shí)产阱、取消信號(hào)以及跨API邊界和進(jìn)程之間request-scope
值婉称。當(dāng)服務(wù)器來新的請(qǐng)求應(yīng)該創(chuàng)建一個(gè)Context
并且返回請(qǐng)求應(yīng)該接受一個(gè)Context
。這其中的函數(shù)調(diào)用鏈必須傳遞Context
對(duì)象心墅,或者是通過WithCancel
酿矢、WithDeadline
、WithTime
或WithValue
衍生的Context
對(duì)象怎燥。當(dāng)一個(gè)Context
取消瘫筐,所有從這個(gè)對(duì)象衍生的Context
都會(huì)被取消。
例如你可以利用context
的這種機(jī)制铐姚,實(shí)現(xiàn)一個(gè)任務(wù)超時(shí)保護(hù)的方法
func main() {
cancelJob(time.Second*1, func() error {
time.Sleep(time.Second * 10)
return nil
})
}
func cancelJob(timeout time.Duration, f func() error) error {
var (
ctx context.Context
cancelFunc context.CancelFunc
)
if timeout > 0 {
ctx, cancelFunc = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancelFunc = context.WithCancel(context.Background())
}
defer cancelFunc()
e := make(chan error, 1)
go func() {
e <- f()
}()
select {
case err := <-e:
return err
case <-ctx.Done():
return ctx.Err()
}
}
Context
使用大致步驟:
- 1 構(gòu)建一個(gè)
Context
對(duì)象,如果你不知道該使用什么Context合適策肝,可以調(diào)用context.Background
或context.TODO
- 2 根據(jù)你的需求可以對(duì)
Context
進(jìn)行包裝衍生- WithCancel :Context可取消
- WithDeadline: Context可設(shè)置截止時(shí)間
- WithTimeout:實(shí)際使用的是WithDeadline
- WithValue: 需要使用Context傳值
- 3 監(jiān)聽ctx.Done這個(gè)channel 一旦Context取消 此channel會(huì)被關(guān)閉
- 4 最后在方法處理完畢時(shí)請(qǐng)及時(shí)調(diào)用cancel方法 方便資源回收
數(shù)據(jù)結(jié)構(gòu)
context包提供了兩種創(chuàng)建Context對(duì)象的便捷方式
- context.Background 無法被取消 沒有值 沒有截止時(shí)間,通常用于主函數(shù)隐绵、初始化之众、測試或者當(dāng)新請(qǐng)求來了作為頂層Context
- context.TODO 當(dāng)你不知道用啥Context的時(shí)候使用
這兩種方式都是一個(gè)emptyCtx
對(duì)象 本質(zhì)上沒啥差別
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
此外context包提供了4個(gè)方法:WithCancel
、WithDeadline
依许、WithTimeout
棺禾、WithValue
,可以對(duì)context進(jìn)行衍生為cancelCtx
峭跳、timerCtx
膘婶、valueCtx
,他們都實(shí)現(xiàn)了context.Context
接口
// Context的方法是線程安全的
type Context interface {
// 返回context何時(shí)需要被取消 ok為false表示deadline未設(shè)置
Deadline() (deadline time.Time, ok bool)
// 當(dāng)context被取消 Done放回一個(gè)關(guān)閉的channel
// Done返回nil 表示當(dāng)前context不能被取消
// Done通常在select語句中使用
Done() <-chan struct{}
// 返回context取消的原因
Err() error
// 返回context中指定的key關(guān)聯(lián)的value蛀醉,未指定返回nil
// 主要用作在進(jìn)程和API邊界間傳遞request-scoped數(shù)據(jù)悬襟,不要用于可選參數(shù)傳遞
// key需要支持相等操作,最好定義為不可到處類型 避免混淆
Value(key interface{}) interface{}
}
這幾個(gè)對(duì)象層次結(jié)構(gòu)
衍生contexts
通過WithCancel
拯刁、WithDeadline
脊岳、WithTimeout
、WithValue
方法衍生的context為原始context提供了取消、傳值割捅、超時(shí)取消等功能奶躯。
WithCancel
通過WithCancel
衍生出新的可取消的Context
對(duì)象
// WithCancel 返回包含父context拷貝和一個(gè)新的channel的context 和一個(gè)cancel函數(shù)
// 當(dāng)返回的cancel函數(shù)被調(diào)用或父context的done channcel關(guān)閉 則WitchCancel返回的context的channel也會(huì)被關(guān)閉
// 當(dāng)操作完成時(shí)應(yīng)該盡快調(diào)用cancel函數(shù) 這樣就可以釋放此context關(guān)聯(lián)的資源
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
// 構(gòu)建父子上下文之間的關(guān)系 保證父上下文取消時(shí)子上下文也會(huì)被取消
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
將Context包裝為可取消的Context
-->cancelCtx
// cancelCtx可以被取消,當(dāng)取消時(shí)棺牧,也會(huì)將其實(shí)現(xiàn)了canceler的子context也取消
type cancelCtx struct {
Context
mu sync.Mutex // 保護(hù)下面的字段
done chan struct{} // 惰性創(chuàng)建 cancel方法第一次調(diào)用時(shí)關(guān)閉
children map[canceler]struct{} // cancel第一次調(diào)用時(shí)置為nil
err error // cancel第一次調(diào)用時(shí)設(shè)置non-nil
}
其中done
這個(gè)channel是在cancel調(diào)用的時(shí)候才會(huì)被初始化巫糙,cancelCtx
子context若可取消需要實(shí)現(xiàn)canceler
接口
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
構(gòu)建可取消context后,后將取消操作進(jìn)行傳播颊乘,如果父context的Done為nil参淹,表示其不可取消直接返回,否則會(huì)調(diào)用parentCancelCtx
直到找到可取消父context 乏悄,若找到
-
若可以找到則
- 且父context已經(jīng)取消則會(huì)調(diào)用子context的cancel方法進(jìn)行取消浙值;
- 且父context未取消則將當(dāng)前子context交給父context管理
-
若找不到 例如開發(fā)者自定義的類型則
直接啟動(dòng)一個(gè)
gorountine
來監(jiān)聽父子取消事件通知
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
if parent.Done() == nil {
return // parent is never canceled
}
if p, ok := parentCancelCtx(parent); ok {//找到父可取消context
p.mu.Lock()
if p.err != nil {
// 父context已經(jīng)被取消 取消子context
child.cancel(false, p.err)
} else {// 父context未cancel 則將子context交給父context管理,方便父節(jié)點(diǎn)取消時(shí)將取消事件傳播給子context
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {//找不到父可取消context
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
parenCancelCtx
循環(huán)查找context是否存在可取消父節(jié)點(diǎn)
// parentCancelCtx follows a chain of parent references until it finds a
// *cancelCtx. This function understands how each of the concrete types in this
// package represents its parent.
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
for {
switch c := parent.(type) {
case *cancelCtx:
return c, true
case *timerCtx:
return &c.cancelCtx, true
case *valueCtx:
parent = c.Context
default:
return nil, false
}
}
}
取消函數(shù)cancel
當(dāng)你的業(yè)務(wù)方法執(zhí)行完畢檩小,你應(yīng)該盡快調(diào)用cancel方法开呐,這樣方便快速回收相關(guān)資源
//關(guān)閉c.done,取消掉c的子context,若removeFromParent為true规求,則將c從父context的子context集合中刪除
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 // c.err不為nil 則表示當(dāng)前context已經(jīng)被取消
}
c.err = err
if c.done == nil { //調(diào)用cancel方法此時(shí)才初始化
c.done = closedchan//closedchan如其名字 為已經(jīng)關(guān)閉的chan
} else {//關(guān)閉c.done
close(c.done)
}
//對(duì)子context進(jìn)行cancel
for child := range c.children {
// 此處在持有父context鎖的同時(shí) 獲取子context的鎖
child.cancel(false, err)
}
c.children = nil //cancel完畢 置nil
c.mu.Unlock()
if removeFromParent {//將當(dāng)前context從其父context的子context集合中刪除
removeChild(c.Context, c)
}
}
WithDeadline
讓context具備超時(shí)取消功能
// WithDeadline 返回包含父context拷貝和deadline為d的context筐付,如果父deadline早于d
// 則語義上WithDeadline(parent, d) 和父context是相等的
//當(dāng)deadline過期了 或者返回的cancel函數(shù)被調(diào)用 或者父context的done channel關(guān)閉了則WithDeadline返回的context中的done channel也會(huì)關(guān)閉
// 當(dāng)操作完成時(shí)應(yīng)該盡快調(diào)用cancel函數(shù) 這樣就可以釋放此context關(guān)聯(lián)的資源
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已經(jīng)到期
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
//定時(shí)監(jiān)聽
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}
timerCtx
內(nèi)嵌了cancelCtx
,主要的cancel能力進(jìn)行了代理阻肿。額外新增了一個(gè)截止時(shí)間和一個(gè)定時(shí)器瓦戚,初始化此類context時(shí)如果未到截止時(shí)間且未取消 則會(huì)啟動(dòng)一個(gè)定時(shí)器,超時(shí)即會(huì)執(zhí)行cancel操作
// timerCtx包含了一個(gè)定時(shí)器和一個(gè)截止時(shí)間 內(nèi)嵌一個(gè)cancelCtx來實(shí)現(xiàn) Done和Err方法
// 取消操作通過停止定時(shí)器然后調(diào)用cancelCtx.cancel來實(shí)現(xiàn)
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
timerCtx
的cancel操作本身會(huì)停掉定時(shí)器丛塌,然后主要cancel操作代理給了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()
}
WithTimeut
實(shí)際掉用了WithDeadline
沒啥好說的较解。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
總的來說,就是可取消context
通過父節(jié)點(diǎn)保存子節(jié)點(diǎn)集合 若父節(jié)點(diǎn)取消則將子節(jié)點(diǎn)集合中的context依次調(diào)用cancel方法赴邻。
WithValue
賦予了Context傳值能力印衔,Context的能力代理給了父context,自身新增了一個(gè)Value(key interface{}) interface{}
方法姥敛,根據(jù)指定key獲取跟context關(guān)聯(lián)的value奸焙,邏輯比較簡單 沒啥好說的。
func WithValue(parent Context, key, val interface{}) Context {
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
Context
key, val interface{}
}
總結(jié)
context
包核心實(shí)現(xiàn)除去注釋只有200-300行彤敛,整體實(shí)現(xiàn)還是短小精悍忿偷,為我們提供了跨進(jìn)程、API邊界的數(shù)據(jù)傳遞以及并發(fā)臊泌、超時(shí)取消等功能。實(shí)際應(yīng)用過程中也給我們技術(shù)實(shí)現(xiàn)帶來很大便利揍拆,比如全鏈路trace的實(shí)現(xiàn)渠概。官方建議我們將context作為函數(shù)第一個(gè)參數(shù)使用,不過實(shí)際使用過程中還是會(huì)給不少人代理心智負(fù)擔(dān),所以有人為了盡可能不寫context播揪,搞了個(gè)Goroutine local storage 有興趣可以研究下