你必須非常努力,才能看起來(lái)毫不費(fèi)力!
在三明等地區(qū),都構(gòu)建了全面的區(qū)域性戰(zhàn)略布局,加強(qiáng)發(fā)展的系統(tǒng)性、市場(chǎng)前瞻性、產(chǎn)品創(chuàng)新能力,以專注、極致的服務(wù)理念,為客戶提供成都網(wǎng)站制作、成都做網(wǎng)站 網(wǎng)站設(shè)計(jì)制作按需制作,公司網(wǎng)站建設(shè),企業(yè)網(wǎng)站建設(shè),高端網(wǎng)站設(shè)計(jì),成都全網(wǎng)營(yíng)銷,成都外貿(mào)網(wǎng)站建設(shè),三明網(wǎng)站建設(shè)費(fèi)用合理。
微信搜索公眾號(hào)[ 漫漫Coding路 ],一起From Zero To Hero !
日常 Go 開(kāi)發(fā)中,Context 包是用的最多的一個(gè)了,幾乎所有函數(shù)的第一個(gè)參數(shù)都是 ctx,那么我們?yōu)槭裁匆獋鬟f Context 呢,Context 又有哪些用法,底層實(shí)現(xiàn)是如何呢?相信你也一定會(huì)有探索的欲望,那么就跟著本篇文章,一起來(lái)學(xué)習(xí)吧!
開(kāi)發(fā)中肯定會(huì)調(diào)用別的函數(shù),比如 A 調(diào)用 B,在調(diào)用過(guò)程中經(jīng)常會(huì)設(shè)置超時(shí)時(shí)間,比如超過(guò)2s 就不等待 B 的結(jié)果了,直接返回,那么我們需要怎么做呢?
// 睡眠5s,模擬長(zhǎng)時(shí)間操作
func FuncB() (interface{}, error) {
time.Sleep(5 * time.Second)
return struct{}{}, nil
}
func FuncA() (interface{}, error) {
var res interface{}
var err error
ch := make(chan interface{})
// 調(diào)用FuncB(),將結(jié)果保存至 channel 中
go func() {
res, err = FuncB()
ch <- res
}()
// 設(shè)置一個(gè)2s的定時(shí)器
timer := time.NewTimer(2 * time.Second)
// 監(jiān)測(cè)是定時(shí)器先結(jié)束,還是 FuncB 先返回結(jié)果
select {
// 超時(shí),返回默認(rèn)值
case <-timer.C:
return "default", err
// FuncB 先返回結(jié)果,關(guān)閉定時(shí)器,返回 FuncB 的結(jié)果
case r := <-ch:
if !timer.Stop() {
<-timer.C
}
return r, err
}
}
func main() {
res, err := FuncA()
fmt.Println(res, err)
}
上面我們的實(shí)現(xiàn),可以實(shí)現(xiàn)超過(guò)等待時(shí)間后,A 不等待 B,但是 B 并沒(méi)有感受到取消信號(hào),如果 B 是個(gè)計(jì)算密度型的函數(shù),我們也希望B 感知到取消信號(hào),及時(shí)取消計(jì)算并返回,減少資源浪費(fèi)。
另一種情況,如果存在多層調(diào)用,比如A 調(diào)用 B、C,B 調(diào)用 D、E,C調(diào)用 E、F,在超過(guò) A 的超時(shí)時(shí)間后,我們希望取消信號(hào)能夠一層層的傳遞下去,后續(xù)所有被調(diào)用到的函數(shù)都能感知到,及時(shí)返回。
在多層調(diào)用的時(shí)候,A->B->C->D,有些數(shù)據(jù)需要固定傳輸,比如 LogID,通過(guò)打印相同的 LogID,我們就能夠追溯某一次調(diào)用,方便問(wèn)題的排查。如果每次都需要傳參的話,未免太麻煩了,我們可以使用 Context 來(lái)保存。通過(guò)設(shè)置一個(gè)固定的 Key,打印日志時(shí)從中取出 value 作為 LogID。
const LogKey = "LogKey"
// 模擬一個(gè)日志打印,每次從 Context 中取出 LogKey 對(duì)應(yīng)的 Value 作為L(zhǎng)ogID
type Logger struct{}
func (logger *Logger) info(ctx context.Context, msg string) {
logId, ok := ctx.Value(LogKey).(string)
if !ok {
logId = uuid.New().String()
}
fmt.Println(logId + " " + msg)
}
var logger Logger
// 日志打印 并 調(diào)用 FuncB
func FuncA(ctx context.Context) {
logger.info(ctx, "FuncA")
FuncB(ctx)
}
func FuncB(ctx context.Context) {
logger.info(ctx, "FuncB")
}
// 獲取初始化的,帶有 LogID 的 Context,一般在程序入口做
func getLogCtx(ctx context.Context) context.Context {
logId, ok := ctx.Value(LogKey).(string)
if ok {
return ctx
}
logId = uuid.NewString()
return context.WithValue(ctx, LogKey, logId)
}
func main() {
ctx = getLogCtx(context.Background())
FuncA(ctx)
}
這利用到了本篇文章講到的 valueCtx,繼續(xù)往下看,一起來(lái)學(xué)習(xí) valueCtx 是怎么實(shí)現(xiàn)的吧!
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Context 接口比較簡(jiǎn)單,定義了四個(gè)方法:
Done() 是一個(gè)比較常用的方法,下面是一個(gè)比較經(jīng)典的流式處理任務(wù)的示例:監(jiān)聽(tīng) ctx.Done() 是否被關(guān)閉來(lái)判斷任務(wù)是否需要取消,需要取消則返回相應(yīng)的原因;沒(méi)有取消則將計(jì)算的結(jié)果寫入到 out channel中。
func Stream(ctx context.Context, out chan<- Value) error {
for {
// 處理數(shù)據(jù)
v, err := DoSomething(ctx)
if err != nil {
return err
}
// ctx.Done() 讀取到數(shù)據(jù),說(shuō)明獲取到了任務(wù)取消的信號(hào)
select {
case <-ctx.Done():
return ctx.Err()
// 否則將結(jié)果輸出,繼續(xù)計(jì)算
case out <- v:
}
}
}
Value() 也是一個(gè)比較常用的方法,用于在上下文中傳遞一些數(shù)據(jù)。使用 context.WithValue() 方法存入 key 和 value,通過(guò) Value() 方法則可以根據(jù) key 拿到 value。
func main() {
ctx := context.Background()
c := context.WithValue(ctx, "key", "value")
v, ok := c.Value("key").(string)
fmt.Println(v, ok)
}
Context 接口并不需要我們自己去手動(dòng)實(shí)現(xiàn),一般我們都是直接使用 context 包中提供的 Background() 方法和 TODO() 方法,來(lái)獲取最基礎(chǔ)的 Context。
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
Background() 方法一般用在 main 函數(shù),或者程序的初始化方法中;在我們不知道使用哪個(gè) Context,或者上文沒(méi)有傳遞 Context時(shí),可以使用 TODO()。
Background() 和 TODO() 都是基于 emptyCtx 生成的,從名字可以看出來(lái),emptyCtx 是一個(gè)空的Context,沒(méi)有 deadline、不能被取消、沒(méi)有鍵值對(duì)。
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"
}
除了上面兩個(gè)最基本的 Context 外,context 包中提供了功能更加豐富的 Context,包括 valueCtx、cancelCtx、timerCtx,下面我們就挨個(gè)來(lái)看下。
使用示例
我們一般使用 context.WithValue() 方法向 Context 存入鍵值對(duì),然后通過(guò) Value() 方法根據(jù) key 得到 value,此種功能的實(shí)現(xiàn)就依賴 valueCtx。
func main() {
ctx := context.Background()
c := context.WithValue(ctx, "myKey", "myValue")
v1 := c.Value("myKey")
fmt.Println(v1.(string))
v2 := c.Value("hello")
fmt.Println(v2) // nil
}
valueCtx 結(jié)構(gòu)體中嵌套了 Context,使用 key 、value 來(lái)保存鍵值對(duì):
type valueCtx struct {
Context
key, val interface{}
}
context包 對(duì)外暴露了 WithValue 方法,基于一個(gè) parent context 來(lái)創(chuàng)建一個(gè) valueCtx。從下面的源碼中可以看出,key 必須是可比較的!
func WithValue(parent Context, key, val interface{}) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
*valueCtx 實(shí)現(xiàn)了 Value(),可以根據(jù) key 得到 value。這是一個(gè)向上遞歸尋找的過(guò)程,如果 key 不在當(dāng)前 valueCtx 中,會(huì)繼續(xù)向上找 parent Context,直到找到最頂層的 Context,一般最頂層的是 emptyCtx,而 emtpyCtx.Value() 返回 nil。
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
cancelCtx 是一個(gè)用于取消任務(wù)的 Context,任務(wù)通過(guò)監(jiān)聽(tīng) Context 是否被取消,來(lái)決定是否繼續(xù)處理任務(wù)還是直接返回。
如下示例中,我們?cè)?main 函數(shù)定義了一個(gè) cancelCtx,并在 2s 后調(diào)用 cancel() 取消 Context,即我們希望 doSomething() 在 2s 內(nèi)完成任務(wù),否則就可以直接返回,不需要再繼續(xù)計(jì)算浪費(fèi)資源了。
doSomething() 方法內(nèi)部,我們使用 select 監(jiān)聽(tīng)任務(wù)是否完成,以及 Context 是否已經(jīng)取消,哪個(gè)先到就執(zhí)行哪個(gè)分支。方法模擬了一個(gè) 5s 的任務(wù),main 函數(shù)等待時(shí)間是2s,因此沒(méi)有完成任務(wù);如果main函數(shù)等待時(shí)間改為10s,則任務(wù)完成并會(huì)返回結(jié)果。
這只是一層調(diào)用,真實(shí)情況下可能會(huì)有多級(jí)調(diào)用,比如 doSomething 可能又會(huì)調(diào)用其他任務(wù),一旦 parent Context 取消,后續(xù)的所有任務(wù)都應(yīng)該取消。
func doSomething(ctx context.Context) (interface{}, error) {
res := make(chan interface{})
go func() {
fmt.Println("do something")
time.Sleep(time.Second * 5)
res <- "done"
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case value := <-res:
return value, nil
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(time.Second * 2)
cancel()
}()
res, err := doSomething(ctx)
fmt.Println(res, err) // nil , context canceled
}
接下來(lái)就讓我們來(lái)研究下,cancelCtx 是如何實(shí)現(xiàn)取消的吧
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
// closedchan is a reusable closed channel.
var closedchan = make(chan struct{})
func init() {
close(closedchan)
}
var Canceled = errors.New("context canceled")
CancelFunc 是一個(gè)函數(shù)類型定義,是一個(gè)取消函數(shù),有如下規(guī)范:
type CancelFunc func()
&cancelCtxKey 是一個(gè)固定的key,用來(lái)返回 cancelCtx 自身
var cancelCtxKey int
cancelCtx 是可以被取消的,它嵌套了 Context 接口,實(shí)現(xiàn)了 canceler 接口。cancelCtx 使用 children 字段保存同樣實(shí)現(xiàn) canceler 接口的子節(jié)點(diǎn),當(dāng) cancelCtx 被取消時(shí),所有的子節(jié)點(diǎn)也會(huì)取消。
type cancelCtx struct {
Context
mu sync.Mutex // 保護(hù)如下字段,保證線程安全
done atomic.Value // 保存 channel,懶加載,調(diào)用 cancel 方法時(shí)會(huì)關(guān)閉這個(gè) channel
children map[canceler]struct{} // 保存子節(jié)點(diǎn),第一次調(diào)用 cancel 方法時(shí)會(huì)置為 nil
err error // 保存為什么被取消,默認(rèn)為nil,第一次調(diào)用 cancel 會(huì)賦值
}
*cancelCtx 的 Value() 方法 和 *valueCtx 的 Value() 方法類似,只不過(guò)加了個(gè)固定的key: &cancelCtxKey。當(dāng)key 為 &cancelCtxKey 時(shí)返回自身
func (c *cancelCtx) Value(key interface{}) interface{} {
if key == &cancelCtxKey {
return c
}
return c.Context.Value(key)
}
*cancelCtx 的 done 字段是懶加載的,只有在調(diào)用 Done() 方法 或者 cancel() 時(shí)才會(huì)賦值。
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
// 如果已經(jīng)有值了,直接返回
if d != nil {
return d.(chan struct{})
}
// 沒(méi)有值,加鎖賦值
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
Err 方法返回 cancelCtx 的 err 字段
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
那么我們?nèi)绾涡陆ㄒ粋€(gè) cancelCtx呢?context 包提供了 WithCancel() 方法,讓我們基于一個(gè) Context 來(lái)創(chuàng)建一個(gè) cancelCtx。WithCancel() 方法返回兩個(gè)字段,一個(gè)是基于傳入的 Context 生成的 cancelCtx,另一個(gè)是 CancelFunc。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
WithCancel 調(diào)用了兩個(gè)外部方法:newCancelCtx 、propagateCancel。newCancelCtx 比較簡(jiǎn)單,根據(jù)傳入的 context,返回了一個(gè) cancelCtx 結(jié)構(gòu)體。
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
propagateCancel 從名字可以看出,就是將 cancel 傳播。如果父Context支持取消,那么我們需要建立一個(gè)通知機(jī)制,這樣父節(jié)點(diǎn)取消的時(shí)候,通知子節(jié)點(diǎn)也取消,層層傳播。
在 propagateCancel 中,如果 父Context 是 cancelCtx 類型且未取消,會(huì)將 子Context 掛在它下面,形成一個(gè)樹結(jié)構(gòu);其余情況都不會(huì)掛載。
func propagateCancel(parent Context, child canceler) {
// 如果 parent 不支持取消,那么就不支持取消傳播,直接返回
done := parent.Done()
if done == nil {
return
}
// 到這里說(shuō)明 done 不為 nil,parent 支持取消
select {
case <-done:
// 如果 parent 此時(shí)已經(jīng)取消了,那么直接告訴子節(jié)點(diǎn)也取消
child.cancel(false, parent.Err())
return
default:
}
// 到這里說(shuō)明此時(shí) parent 還未取消
// 如果 parent 是未取消的 cancelCtx
if p, ok := parentCancelCtx(parent); ok {
// 加鎖,防止并發(fā)更新
p.mu.Lock()
// 再次判斷,因?yàn)橛锌赡苌弦粋€(gè)獲得鎖的進(jìn)行了取消操作。
// 如果 parent 已經(jīng)取消了,那么子節(jié)點(diǎn)也直接取消
if p.err != nil {
child.cancel(false, p.err)
} else {
// 把子Context 掛到父節(jié)點(diǎn) parent cancelCtx 的 children字段下
// 之后 parent cancelCtx 取消時(shí),能通知到所有的 子Context
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
// parent 不是 cancelCtx 類型,可能是用戶自己實(shí)現(xiàn)的Context
atomic.AddInt32(&goroutines, +1)
// 啟動(dòng)一個(gè)協(xié)程監(jiān)聽(tīng),如果 parent 取消了,子 Context 也取消
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
cancel 方法就是來(lái)取消 cancelCtx,主要的工作是:關(guān)閉c.done 中的channel,給 err 賦值,然后級(jí)聯(lián)取消所有 子Context。如果 removeFromParent 為 true,會(huì)從父節(jié)點(diǎn)中刪除以該節(jié)點(diǎn)為樹頂?shù)臉洹?/p>
cancel() 方法只負(fù)責(zé)自己管轄的范圍,即自己以及自己的子節(jié)點(diǎn),然后根據(jù)配置判斷是否需要從父節(jié)點(diǎn)中移除自己為頂點(diǎn)的樹。如果子節(jié)點(diǎn)還有子節(jié)點(diǎn),那么由子節(jié)點(diǎn)負(fù)責(zé)處理,不用自己負(fù)責(zé)了。
propagateCancel() 中有三處調(diào)用了 cancel() 方法,傳入的 removeFromParent 都為 false,是因?yàn)楫?dāng)時(shí)根本沒(méi)有掛載,不需要移除。而 WithCancel 返回的 CancelFunc ,傳入的 removeFromParent 為 true,是因?yàn)檎{(diào)用 propagateCancel 有可能產(chǎn)生掛載,當(dāng)產(chǎn)生掛載時(shí),調(diào)用 cancel() 就需要移除了。
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// err 是指取消的原因,必傳,cancelCtx 中是 errors.New("context canceled")
if err == nil {
panic("context: internal error: missing cancel error")
}
// 涉及到保護(hù)字段值的修改,都需要加鎖
c.mu.Lock()
// 如果該Context已經(jīng)取消過(guò)了,直接返回。多次調(diào)用cancel,不會(huì)產(chǎn)生額外效果
if c.err != nil {
c.mu.Unlock()
return
}
// 給 err 賦值,這里 err 一定不為 nil
c.err = err
// close channel
d, _ := c.done.Load().(chan struct{})
// 因?yàn)閏.done 是懶加載,有可能存在 nil 的情況
// 如果 c.done 中沒(méi)有值,直接賦值 closedchan;否則直接 close
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
// 遍歷當(dāng)前 cancelCtx 所有的子Context,讓子節(jié)點(diǎn)也 cancel
// 因?yàn)楫?dāng)前的Context 會(huì)主動(dòng)把子Context移除,子Context 不用主動(dòng)從parent中脫離
// 因此 child.cancel 傳入的 removeFromParent 為false
for child := range c.children {
child.cancel(false, err)
}
// 將 children 置空,相當(dāng)于移除自己的所有子Context
c.children = nil
c.mu.Unlock()
// 如果當(dāng)前 cancelCtx 需要從上層的 cancelCtx移除,調(diào)用removeChild方法
// c.Context 就是自己的父Context
if removeFromParent {
removeChild(c.Context, c)
}
}
從propagateCancel方法中可以看到,只有parent 屬于 cancelCtx 類型 ,才會(huì)將自己掛載。因此 removeChild 會(huì)再次判斷 parent 是否為 cancelCtx,和之前的邏輯保持一致。找到的話,再將自己移除,需要注意的是,移除會(huì)把自己及其自己下面的所有子節(jié)點(diǎn)都移除。
如果上一步 propagateCancel 方法將自己掛載到了 A 上,但是在調(diào)用 cancel() 時(shí),A 已經(jīng)取消過(guò)了,此時(shí) parentCancelCtx() 會(huì)返回 false。不過(guò)這沒(méi)有關(guān)系,A 取消時(shí)已經(jīng)將掛載的子節(jié)點(diǎn)移除了,當(dāng)前的子節(jié)點(diǎn)不用將自己從 A 中移除了。
func removeChild(parent Context, child canceler) {
// parent 是否為未取消的 cancelCtx
p, ok := parentCancelCtx(parent)
if !ok {
return
}
// 獲取 parent cancelCtx 的鎖,修改保護(hù)字段 children
p.mu.Lock()
// 將自己從 parent cancelCtx 的 children 中刪除
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
parentCancelCtx 判斷 parent 是否為 未取消的 *cancelCtx。取消與否容易判斷,難判斷的是 parent 是否為 *cancelCtx,因?yàn)橛锌赡芷渌Y(jié)構(gòu)體內(nèi)嵌了 cancelCtx,比如 timerCtx,會(huì)通過(guò)比對(duì) channel 來(lái)確定。
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
// 如果 parent context 的 done 為 nil, 說(shuō)明不支持 cancel,那么就不可能是 cancelCtx
// 如果 parent context 的 done 為 closedchan, 說(shuō)明 parent context 已經(jīng) cancel 了
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
// 到這里說(shuō)明支持取消,且沒(méi)有被取消
// 如果 parent context 屬于原生的 *cancelCtx 或衍生類型,需要繼續(xù)進(jìn)行后續(xù)判斷
// 如果 parent context 無(wú)法轉(zhuǎn)換到 *cancelCtx,則認(rèn)為非 cancelCtx,返回 nil,fasle
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
// 經(jīng)過(guò)上面的判斷后,說(shuō)明 parent context 可以被轉(zhuǎn)換為 *cancelCtx,這時(shí)存在多種情況:
// - parent context 就是 *cancelCtx
// - parent context 是標(biāo)準(zhǔn)庫(kù)中的 timerCtx
// - parent context 是個(gè)自己自定義包裝的 cancelCtx
//
// 針對(duì)這 3 種情況需要進(jìn)行判斷,判斷方法就是:
// 判斷 parent context 通過(guò) Done() 方法獲取的 done channel 與 Value 查找到的 context 的 done channel 是否一致
//
// 一致情況說(shuō)明 parent context 為 cancelCtx 或 timerCtx 或 自定義的 cancelCtx 且未重寫 Done(),
// 這種情況下可以認(rèn)為拿到了底層的 *cancelCtx
//
// 不一致情況說(shuō)明 parent context 是一個(gè)自定義的 cancelCtx 且重寫了 Done() 方法,并且并未返回標(biāo)準(zhǔn) *cancelCtx 的
// 的 done channel,這種情況需要單獨(dú)處理,故返回 nil, false
pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
return nil, false
}
return p, true
}
簡(jiǎn)介
timerCtx 嵌入了 cancelCtx,并新增了一個(gè) timer 和 deadline 字段。timerCtx 的取消能力是復(fù)用 cancelCtx 的,只是在這個(gè)基礎(chǔ)上增加了定時(shí)取消而已。
在我們的使用過(guò)程中,有可能還沒(méi)到 deadline,任務(wù)就提前完成了,此時(shí)需要手動(dòng)調(diào)用 CancelFunc。
func slowOperationWithTimeout(ctx context.Context) (Result, error) {
ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel() // 如果未到截止時(shí)間,slowOperation就完成了,盡早調(diào)用 cancel() 釋放資源
return slowOperation(ctx)
}
type timerCtx struct {
cancelCtx // 內(nèi)嵌 cancelCtx
timer *time.Timer // 受 cancelCtx.mu 互斥鎖的保護(hù)
deadline time.Time // 截止時(shí)間
}
Deadline() 返回 deadline 字段的值
func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
return c.deadline, true
}
WithDeadline 基于parent Context 和 時(shí)間點(diǎn) d,返回了一個(gè)定時(shí)取消的 Context,以及一個(gè) CancelFunc。返回的Context 有三種情況被取消:1. 到達(dá)了指定時(shí)間,就會(huì)主動(dòng)取消;2. 手動(dòng)調(diào)用了 CancelFunc;3. 父Context取消,導(dǎo)致該Context被取消。這三種情況哪種先到,就會(huì)首次觸發(fā)取消操作,后續(xù)的再次取消不會(huì)產(chǎn)生任何效果。
如果傳入 parent Context 的 deadline 比指定的時(shí)間 d 還要早,此時(shí) d 就沒(méi)用處了,直接依賴 parent 取消傳播就可以了。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// 傳入的 parent 不能為 nil
if parent == nil {
panic("cannot create context from nil parent")
}
// parent 也有 deadline,并且比 d 還要早,直接依賴 parent 的取消傳播即可
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
// 定義 timerCtx 接口
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 設(shè)置傳播,如果parent 屬于 cancelCtx,會(huì)掛載到 children 字段上
propagateCancel(parent, c)
// 距離截止時(shí)間 d 還有多久
dur := time.Until(d)
if dur <= 0 {
// 已經(jīng)到了截止時(shí)間,直接取消,同時(shí)從 parent 中取消掛載
// 由于是超時(shí),取消時(shí)的 err 是 DeadlineExceeded
c.cancel(true, DeadlineExceeded)
// 再返回 c 和 CancelFunc,已經(jīng)取消掛載了,此時(shí)的 CancelFunc 不會(huì)從 parent 中取消掛載
// 后面再次調(diào)用 CancelFunc 不會(huì)產(chǎn)生任何效果了
// 主動(dòng)取消的話,err 是 Canceled
return c, func() { c.cancel(false, Canceled) }
}
// 還沒(méi)有到截止時(shí)間,定義一個(gè)定時(shí)器,過(guò)了 dur 會(huì)自動(dòng)取消
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
// 由于是到了截止時(shí)間才取消,err 是 DeadlineExceeded
c.cancel(true, DeadlineExceeded)
})
}
// 返回 c 和 cancelFunc,主動(dòng)取消的 err 是 Canceled
return c, func() { c.cancel(true, Canceled) }
}
接下來(lái)我們看下 cancel 方法,timerCtx 的 cancel 方法 就是調(diào)用內(nèi)嵌 cancelCtx 的 cancel() 方法,默認(rèn)是不從父節(jié)點(diǎn)移除
func (c *timerCtx) cancel(removeFromParent bool, err error) {
c.cancelCtx.cancel(false, err)
// 從父節(jié)點(diǎn)中移除
if removeFromParent {
removeChild(c.cancelCtx.Context, c)
}
// 把定時(shí)器停了,釋放資源
// 有可能還沒(méi)到deadline,手動(dòng)觸發(fā)了 CancelFunc,此時(shí)把 timer 停了
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
WithTimeout 就是基于 WithDeadline,deadline 就是基于當(dāng)前時(shí)間計(jì)算的
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
本篇文章,我們通過(guò)源碼+示例的方式,一起學(xué)習(xí)了 context 包相關(guān)的結(jié)構(gòu)以及實(shí)現(xiàn)邏輯,包括如下內(nèi)容
Context 接口:定義了一些接口方法和規(guī)范
emptyCtx:空的Context,Background() 和 TODO() 方法就是使用的 emptyCtx
valueCtx:用于保存鍵值對(duì),查詢時(shí)是遞歸查詢,可以用于 LogID 這種全局 id 的保存
cancelCtx:可以取消的Context,用于取消信號(hào)的傳遞
timerCtx:定時(shí)取消的 cancelCtx
個(gè)人博客: https://lifelmy.github.io/
微信公眾號(hào):漫漫Coding路