最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

深入理解Golang中Context的使用

 更新時間:2026年02月25日 10:12:39   作者:武大打工仔  
本文主要介紹了Golang中Context的使用,特別是cancelCtx的實(shí)現(xiàn),通過理解cancelCtx的數(shù)據(jù)結(jié)構(gòu)、掛載機(jī)制和取消機(jī)制,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

理解 cancelCtx 的源碼,其實(shí)就是理解 Go 語言如何優(yōu)雅地處理并發(fā)控制信號廣播。

Go 的 context 源碼在 src/context/context.go 中,代碼量不多(不到 600 行),但設(shè)計非常精妙。核心邏輯可以歸納為三個關(guān)鍵詞:掛載(Mount)、廣播(Broadcast)、遞歸(Recursion)。

我們分三步來拆解 cancelCtx 的底層原理。

1 第一步:數(shù)據(jù)結(jié)構(gòu) —— 它是怎么長的?

cancelCtx 是一個結(jié)構(gòu)體,它繼承了父 Context,同時自己維護(hù)了一套“家譜”關(guān)系。

// 源碼簡化版
type cancelCtx struct {
    Context             // 1. 嵌入父 Context,保證能調(diào)用父類方法
    mu       sync.Mutex // 2. 互斥鎖,保證并發(fā)安全(保護(hù)下面的字段)
    done     chan struct{} // 3. 核心!用來發(fā)信號的 Channel
    children map[canceler]struct{} // 4. 存自己的“孩子”,以便取消時通知它們
    err      error      // 5. 記錄取消原因(Canceled 還是 DeadlineExceeded)
}
  • done:這是一個 chan struct{}。這利用了 Go 的一個特性:當(dāng)一個 channel 被 close(關(guān)閉)時,所有監(jiān)聽這個 channel 的 goroutine 都會收到信號(讀到零值)。 這就是它的“廣播”機(jī)制。
  • children:這是一個 Set(用 Map 實(shí)現(xiàn))。當(dāng)父節(jié)點(diǎn)取消時,它需要遍歷這個 Map,把所有的子節(jié)點(diǎn)也干掉。

2 第二步:掛載機(jī)制 —— 如何與父節(jié)點(diǎn)建立聯(lián)系?

當(dāng)你調(diào)用 context.WithCancel(parent) 時,核心邏輯在于如何把自己“掛”到父節(jié)點(diǎn)的 children 列表里。

這主要依賴于內(nèi)部函數(shù) propagateCancel(傳播取消)。

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    c := newCancelCtx(parent) // 創(chuàng)建自己
    propagateCancel(parent, &c) // 【關(guān)鍵】把自己掛到父節(jié)點(diǎn)上
    return &c, func() { c.cancel(true, Canceled) }
}

propagateCancel 的邏輯如下:

  1. 判斷父節(jié)點(diǎn)狀態(tài):如果父節(jié)點(diǎn) parent.Done() 已經(jīng)是 nil(永遠(yuǎn)不會取消,比如 Background()),那就不用掛了,因為父親永遠(yuǎn)不死。如果父親已經(jīng)死了,直接提前 cancel 并返回;
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err(), Cause(parent))
		return
	default:
	}
  1. 查找祖先:它會嘗試找到最近的、也就是標(biāo)準(zhǔn)的 *cancelCtx 類型的祖先。
	if p, ok := parentCancelCtx(parent); ok {
		// parent is a *cancelCtx, or derives from one.
		p.mu.Lock()
		if err := p.err.Load(); err != nil {
			// parent has already been canceled
			child.cancel(false, err.(error), p.cause)
		} else {
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
		return
	}
  1. 判斷是否實(shí)現(xiàn)了 afterFuncer 接口
	if a, ok := parent.(afterFuncer); ok {
		// parent implements an AfterFunc method.
		c.mu.Lock()
		stop := a.AfterFunc(func() {
			child.cancel(false, parent.Err(), Cause(parent))
		})
		c.Context = stopCtx{
			Context: parent,
			stop:    stop,
		}
		c.mu.Unlock()
		return
	}
  1. 掛載
  • 如果找到了親爹(Go 標(biāo)準(zhǔn)庫的 Context):加鎖,把自己塞到父親的 children map 里。
	if a, ok := parent.(afterFuncer); ok {
		// parent implements an AfterFunc method.
		c.mu.Lock()
		stop := a.AfterFunc(func() {
			child.cancel(false, parent.Err(), Cause(parent))
		})
		c.Context = stopCtx{
			Context: parent,
			stop:    stop,
		}
		c.mu.Unlock()
		return
	}
  • 如果父親是外人(比如自定義的 Context):沒辦法直接操作它的 map。于是啟動一個 Goroutine,在那傻傻地監(jiān)聽 parent.Done()。一旦父親 Done 了,這個 Goroutine 就負(fù)責(zé)把自己干掉。
	goroutines.Add(1)
	go func() {
		select {
		case <-parent.Done():
			child.cancel(false, parent.Err(), Cause(parent))
		case <-child.Done():
		}
	}()

3 第三步:取消機(jī)制 —— 信號是如何傳播的?

源碼中的 cancel 方法(簡化版邏輯):

// removeFromParent: 是否需要把自己從父親的 map 中移除
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    c.mu.Lock()
    if c.err != nil { // 如果已經(jīng)取消過了,直接返回
        c.mu.Unlock()
        return
    }
    c.err = err // 記錄錯誤原因

    // 1. 【核心動作】關(guān)閉 channel!
    // 此時,所有監(jiān)聽 <-ctx.Done() 的協(xié)程都會瞬間收到信號
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }

    // 2. 【遞歸通知】遍歷所有孩子,挨個調(diào)用它們的 cancel
    for child := range c.children {
        // 遞歸調(diào)用孩子的 cancel 方法
        // 注意:這里傳 false,意思是孩子不需要從我這里移除(因為我自己都要沒了,大家一起銷毀)
        child.cancel(false, err) 
    }
    c.children = nil // 釋放 map,利用 GC 回收內(nèi)存
    c.mu.Unlock()

    // 3. 【斷絕關(guān)系】把自己從父親的 map 里刪掉
    // 避免父親一直持有我的引用,導(dǎo)致內(nèi)存泄漏
    if removeFromParent {
        removeChild(c.Context, c)
    }
}

4 總結(jié):Context 的生命周期圖解

假設(shè)有這樣一個調(diào)用鏈:CtxA (根) -> CtxB -> CtxC。

  1. 建立連接

    • CtxB 創(chuàng)建時,將自己放入 CtxA.children。
    • CtxC 創(chuàng)建時,將自己放入 CtxB.children
  2. 觸發(fā)取消 (CtxB.cancel() 被調(diào)用):

    • 自身CtxB 關(guān)閉自己的 done channel。所有監(jiān)聽 CtxB 的代碼收到停止信號。
    • 向下(遞歸)CtxB 遍歷 children,找到 CtxC,調(diào)用 CtxC.cancel()。于是 CtxC 也關(guān)閉 done channel。
    • 向上(清理)CtxB 調(diào)用 removeChild,讓 CtxACtxB 從 map 中刪掉。

5 補(bǔ)充

我們主動調(diào)用 cancel 的時候,removeFromParent 是不是傳 true ?

回答:是的

當(dāng)我們我們在業(yè)務(wù)代碼中調(diào)用那個由 context.WithCancel(或 WithTimeout、WithDeadline)返回的 cancel() 函數(shù)時,傳入 removeFromParent 的參數(shù)確實(shí)是 true

這一步非常關(guān)鍵,它是防止 Context 樹內(nèi)存泄漏 的核心手段。

我們來看一下源碼驗證,順便對比一下什么時候傳 false。

1. 證據(jù):源碼中的入口

當(dāng)你調(diào)用 context.WithCancel 時,源碼是這樣寫的:

// src/context/context.go

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	c := newCancelCtx(parent)
	propagateCancel(parent, &c) // 把自己掛到父親節(jié)點(diǎn)
	
	// 【注意看這里】
	// 返回的閉包函數(shù),硬編碼了 true
	return &c, func() { c.cancel(true, Canceled) }
}

解析: 這里的 func() { c.cancel(true, Canceled) } 就是你拿到的那個 cancel 函數(shù)。 當(dāng)你調(diào)用它時,你告訴 cancelCtx:“我已經(jīng)干完活了(或者想終止了),請把我從父親的 children 列表中移除。

如果這里不傳 true,父節(jié)點(diǎn)就會一直保留著指向這個子節(jié)點(diǎn)的引用,直到父節(jié)點(diǎn)自己被取消。如果父節(jié)點(diǎn)是一個全局的 Background 或者生命周期很長的 Context,那么這些早已死掉的子節(jié)點(diǎn)就會一直堆積在內(nèi)存里,導(dǎo)致內(nèi)存泄漏。

2. 對比:什么時候傳false?

cancel 方法內(nèi)部,當(dāng)它遞歸去取消自己的 子節(jié)點(diǎn) 時,傳的是 false。

// src/context/context.go -> cancel 方法內(nèi)部

// ... 前面是關(guān)閉 channel 的邏輯

// 遍歷所有的孩子
for child := range c.children {
    // 【注意看這里】
    // 父親取消孩子時,傳的是 false
    child.cancel(false, err) 
}
c.children = nil // 父親直接清空整個 map
c.mu.Unlock()

// 如果 removeFromParent 為 true,才去調(diào)用 removeChild
if removeFromParent {
    removeChild(c.Context, c)
}

為什么這里傳 false?這是一個性能優(yōu)化的設(shè)計。

想象一下這個場景:

  1. 父節(jié)點(diǎn)取消了。
  2. 父節(jié)點(diǎn)需要通知它下面的 1000 個子節(jié)點(diǎn)也取消。
  3. 父節(jié)點(diǎn)遍歷這 1000 個子節(jié)點(diǎn),調(diào)用它們的 cancel
  4. 關(guān)鍵點(diǎn):父節(jié)點(diǎn)在通知完所有孩子后,緊接著有一行 c.children = nil,它會直接把整個“孩子名冊”撕碎丟進(jìn)垃圾桶。

如果此時給子節(jié)點(diǎn)傳 true,那么這 1000 個子節(jié)點(diǎn)每一個都會回頭去搶父節(jié)點(diǎn)的鎖(c.Context),試圖從父節(jié)點(diǎn)的 map 中刪除自己。 但在父節(jié)點(diǎn)看來:“你們不用一個個來辭職了,反正整個部門我都要裁掉,名單我已經(jīng)準(zhǔn)備銷毀了。

所以,傳 false 避免了成百上千次無意義的鎖競爭和 Map 刪除操作。

總結(jié):

  • 主動調(diào)用(外部觸發(fā))removeFromParent = true。因為只有這一個節(jié)點(diǎn)要走,必須精準(zhǔn)地從父節(jié)點(diǎn)的名單里刪掉,防止內(nèi)存泄漏。
  • 級聯(lián)取消(內(nèi)部遞歸)removeFromParent = false。因為父節(jié)點(diǎn)隨后會直接清空整個名單,子節(jié)點(diǎn)不需要自己去注銷。

到此這篇關(guān)于深入理解Golang中Context的使用的文章就介紹到這了,更多相關(guān)Golang Context使用內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • go?doudou應(yīng)用中使用枚舉類型教程示例

    go?doudou應(yīng)用中使用枚舉類型教程示例

    這篇文章主要為大家介紹了go?doudou應(yīng)用中使用枚舉類型教程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2022-12-12
  • Go語言中時間time相關(guān)處理方法詳解

    Go語言中時間time相關(guān)處理方法詳解

    在Go語言中,time?包是處理時間和日期的核心,它提供了豐富的函數(shù)和方法,用于顯示、測量、計算、格式化、解析時間等,本文給大家詳細(xì)介紹了Go時間time相關(guān)處理方法的相關(guān)資料,需要的朋友可以參考下
    2024-10-10
  • 聊聊Golang性能分析工具pprof的使用

    聊聊Golang性能分析工具pprof的使用

    對于線上穩(wěn)定運(yùn)行的服務(wù)來說,?可能會遇到?cpu、mem?利用率升高的問題,那我們就需要使用?pprof?工具來進(jìn)行性能分析,所以本文就來和大家講講pprof的具體使用吧
    2023-05-05
  • Go語言中函數(shù)可變參數(shù)(Variadic Parameter)詳解

    Go語言中函數(shù)可變參數(shù)(Variadic Parameter)詳解

    在Python中,在函數(shù)參數(shù)不確定數(shù)量的情況下,可以動態(tài)在函數(shù)內(nèi)獲取參數(shù)。在Go語言中,也有類似的實(shí)現(xiàn)方式,本文就來為大家詳細(xì)講解一下
    2022-07-07
  • Go語言開發(fā)實(shí)現(xiàn)一個圖片批量壓縮工具

    Go語言開發(fā)實(shí)現(xiàn)一個圖片批量壓縮工具

    在日常開發(fā)和辦公中,圖片體積過大會帶來不少困擾,本文將使用Go語言編寫一個圖片批量壓縮工具,文中的示例代碼講解詳細(xì),需要的可以了解下
    2025-09-09
  • Go語言應(yīng)用閉包之返回函數(shù)

    Go語言應(yīng)用閉包之返回函數(shù)

    這篇文章主要介紹了Go語言應(yīng)用閉包之返回函數(shù),對于非常底層的純 Go 語言代碼或者包而言,在各個操作系統(tǒng)平臺上的可移植性是非常強(qiáng)的,只需要將源碼拷貝到相應(yīng)平臺上進(jìn)行編譯即可,或者可以使用交叉編譯來構(gòu)建目標(biāo)平臺的應(yīng)用程序,需要的朋友可以參考下
    2023-07-07
  • Golang在圖像中繪制矩形框的示例代碼

    Golang在圖像中繪制矩形框的示例代碼

    這篇文章主要介紹了Golang在圖像中繪制矩形框的示例代碼,文中有詳細(xì)的代碼示例供大家參考,具有一定的參考價值,需要的朋友可以參考下
    2008-08-08
  • 詳解如何在Golang中執(zhí)行shell命令

    詳解如何在Golang中執(zhí)行shell命令

    這篇文章主要為大家詳細(xì)介紹了在 golang 中執(zhí)行 shell 命令的多種方法和場景,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2024-02-02
  • golang 自旋鎖的實(shí)現(xiàn)

    golang 自旋鎖的實(shí)現(xiàn)

    這篇文章主要介紹了golang 自旋鎖的實(shí)現(xiàn),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-11-11
  • go語言調(diào)用c語言的so動態(tài)庫的實(shí)現(xiàn)

    go語言調(diào)用c語言的so動態(tài)庫的實(shí)現(xiàn)

    在Go語言開發(fā)過程中,有時需要調(diào)用C或C++編寫的so動態(tài)庫,本文介紹了如何在Go語言中調(diào)用so庫的步驟和注意事項,包括環(huán)境準(zhǔn)備、編譯生成.so文件、Go文件編寫、以及可能遇到的問題和解決方法,感興趣的可以了解一下
    2024-10-10

最新評論

上虞市| 阳东县| 安岳县| 南昌市| 莱西市| 孝义市| 文昌市| 凉山| 马山县| 乌兰浩特市| 沈丘县| 西乌珠穆沁旗| 万山特区| 城步| 上栗县| 孝感市| 玉林市| 苍梧县| 江北区| 宜章县| 南川市| 隆回县| 津市市| 将乐县| 鹿泉市| 墨江| 玛曲县| 阿图什市| 邢台市| 闵行区| 天峻县| 静宁县| 潞西市| 盐亭县| 二连浩特市| 隆子县| 军事| 大邑县| 收藏| 阳东县| 三穗县|