欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

詳解Go語言中上下文context的理解與使用

 更新時間:2023年10月31日 10:49:08   作者:燈火消逝的碼頭  
在Go的日常開發(fā)中,Context上下文對象無處不在,這篇文章小編就來帶大家深入了解一下上下文context的理解與使用,文中的示例代碼講解詳細,需要的可以參考下

為什么需要 context

在 Go 程序中,特別是并發(fā)情況下,由于超時、取消等而引發(fā)的異常操作,往往需要及時的釋放相應(yīng)資源,正確的關(guān)閉 goroutine。防止協(xié)程不退出而導(dǎo)致內(nèi)存泄露。如果沒有 context,用來控制協(xié)程退出將會非常麻煩,我們來舉一個例子。

假如說現(xiàn)在一個協(xié)程A開啟了一個子協(xié)程B,這個子協(xié)程B又開啟了另外兩個子協(xié)程B1和B2來運行不同的任務(wù),協(xié)程B2又開啟了協(xié)程C來運行其他任務(wù),現(xiàn)在協(xié)程A通知子協(xié)程B該退出了,這個時候我們需要完成這樣的操作:A通知B退出,B退出時通知B1、B2退出,B2退出時通知C退出:

func TestChanCloseGoroutine(t *testing.T) {
	fmt.Printf("開始了,有%d個協(xié)程\n", runtime.NumGoroutine())

	var (
		chB  = make(chan struct{})
		chB1 = make(chan struct{})
		chB2 = make(chan struct{})
		chC  = make(chan struct{})
	)

	// 協(xié)程A
	go func() {
		// 協(xié)程B
		go func() {
			// 協(xié)程B1
			go func() {
				for {
					select {
					case <-chB1:
						return
					default:
					}
				}
			}()
			// 協(xié)程B2
			go func() {
				// 協(xié)程C
				go func() {
					for {
						select {
						case <-chC:
							return
						default:
						}
					}
				}()
				for {
					select {
					case <-chB2:
						// 通知協(xié)程C退出
						chC <- struct{}{}
						return
					default:
					}
				}
			}()

			for {
				select {
				case <-chB:
					chB1 <- struct{}{}
					chB2 <- struct{}{}
					return
				default:
				}
			}
		}()

		// 1秒后通知B退出
		time.Sleep(1 * time.Second)
		chB <- struct{}{}
		// A后續(xù)沒有任務(wù)了,會自動退出
	}()

	time.Sleep(2 * time.Second)
	fmt.Printf("最終結(jié)束,有%d個協(xié)程\n", runtime.NumGoroutine())
}


// 結(jié)果
開始了,有2個協(xié)程
最終結(jié)束,有2個協(xié)程
// tips: Go Test 會啟動兩個額外的 goroutine 來運行代碼,所以初始就會有2個 goroutine

通過 channel 來控制各個 goroutine 的關(guān)閉,程序看上去一點也不優(yōu)雅。而且這才僅僅四個 goroutine ,就已經(jīng)顯得有些力不從心了,在真實的業(yè)務(wù)中,哪怕一個簡單的 http 請求,都不可能啟用四個 goroutine 就能夠完成,且子協(xié)程的層級也絕非只有寥寥的三層!

context 是什么

context 在 Go 中是一個接口,它的定義如下:

type Context interface {
	Deadline() (deadline time.Time, ok bool)
	Done() <-chan struct{}
	Err() error
	Value(key any) any
}
  • Deadline 用來獲取 ctx 的截止時間,如果沒有截至?xí)r間,ok 將返回 false;
  • Done 里面是一個通道,當(dāng) ctx 被取消時,會返回一個關(guān)閉的 channel,如果該 ctx 永遠都不會被關(guān)閉,則返回 nil;
  • Err 返回的 ctx 取消的原因,如果 ctx 沒有被取消,會返回 nil。如果已經(jīng)關(guān)閉了,會返回被關(guān)閉的原因,如果是被取消的會返回 canceled,超時的顯示 deadline exceeded;
  • Value 會返回 ctx 中儲存的值,會從當(dāng)前 ctx 中一路向上追溯,如果整條 ctx 鏈中都沒有找到值,則會返回nil。

context 的基本結(jié)構(gòu)比較簡單,里面也只有四個方法,如果到此沒有理解四個方法也沒有關(guān)系,下文會使用到這四個方法,屆時將會很自然的掌握它們。

context 接口的實現(xiàn)

context 有四個不同的實現(xiàn):emptyCtx、cancelCtx、timerCtx、valueCtx:

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 any) any {
	return nil
}

emptyCtx 是一個實現(xiàn)了 context 接口的整型,它不能儲存信息,也不能被取消,它被當(dāng)作根節(jié)點 ctx。cancelCtx、timerCtx、valueCtx 由于篇幅原因,這里不放出它們的源碼,只解釋它們的作用:cancelCtx 是一個可以主動取消的 ctx。timerCtx 也是一個可以主動取消的 ctx,不同于 cancelCtx,它還儲存著額外的時間信息,當(dāng)時間條件滿足后,會自動取消該 ctx,利用這點,可以實現(xiàn)超時機制。valueCtx 比較簡單,用來創(chuàng)建一個攜帶鍵值的 ctx。

context 的基本使用

創(chuàng)建一個根節(jié)點

創(chuàng)建根節(jié)點有兩種方法:

ctx := context.Background()
ctx := context.TODO()

這兩種方法其實本質(zhì)上都是初始化了一個 emptyCtx:

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

可以看到,在代碼中,這兩個函數(shù)其實是一模一樣的,只是用于不同場景下:Background 推薦在主函數(shù)、初始化和測試中使用,TODO 用于不清楚使用哪個 context 時使用。根節(jié)點 ctx 不具備任何意義,也不能被取消。

創(chuàng)建一個子 ctx

可以通過WithCancel、WithDeadline、WithTimeout、WithValue 這四個主要的函數(shù)來創(chuàng)建子 ctx ,創(chuàng)建一個子 ctx 必須指定其歸屬的父 ctx,由此來形成一個上下文鏈,用來同步 goroutine 信號。來看一下它們的簡單使用:

WithCancel 用來創(chuàng)建一個 cancelCtx,它可以被主動取消 :

func TestCtxWithCancel(t *testing.T) {
	ctx := context.Background()
	ctx, cancel := context.WithCancel(ctx)
	go func() {
		for {
			select {
			// 還記得前文提到的Done的方法嗎
			// 當(dāng) ctx 取消時,ctx.Done()對應(yīng)的通道就會關(guān)閉,case也就會被執(zhí)行
			case <-ctx.Done():
				// ctx.Err() 會獲取到關(guān)閉原因哦
				fmt.Println("協(xié)程關(guān)閉", ctx.Err())
				return
			default:
				fmt.Println("繼續(xù)運行")
				time.Sleep(100 * time.Millisecond)
			}
		}
	}()

	// 等待一秒后關(guān)閉
	time.Sleep(1 * time.Second)
	cancel()
	// 等待一秒,讓子協(xié)程有時間打印出協(xié)程關(guān)閉的原因
	time.Sleep(1 * time.Second)
}

// 結(jié)果
繼續(xù)運行
繼續(xù)運行
……
協(xié)程關(guān)閉 context canceled

WithDeadline 用來創(chuàng)建一個 timerCtx,當(dāng)時間條件滿足后,它會被自動取消 :

func TestCtxWithDeadline(t *testing.T) {
	ctx := context.Background()
	// 等待2秒后自動關(guān)閉
	ctx, cancel := context.WithDeadline(ctx, time.Now().Add(2*time.Second))
	defer cancel()
	// Deadline 前文也提到了,還記得嗎?用來獲取當(dāng)前任務(wù)的截至?xí)r間
	if t, ok := ctx.Deadline(); ok {
		// time.DateTime 是 go1.20 版本的一個常量,其值是:"2006-01-02 15:04:05"
		fmt.Println(t.Format(time.DateTime))
	}
	go func() {
		select {
		case <-ctx.Done():
			// 手動關(guān)閉 context canceled
			// 自動關(guān)閉 context deadline exceeded
			fmt.Println("協(xié)程關(guān)閉", ctx.Err())
			return
		}
	}()

	time.Sleep(3 * time.Second)
}
// 結(jié)果
2023-05-10 18:00:36
協(xié)程關(guān)閉 context deadline exceeded


// 將最后的等待時間更改為一秒
func TestCtxWithDeadline(t *testing.T) {
	……
	time.Sleep(1 * time.Second)
}
// 結(jié)果
2023-05-10 18:01:45
協(xié)程關(guān)閉 context canceled

哪怕 WithDeadline 到達指定時間會自動關(guān)閉,但依然推薦使用 defer cancel() 。這是因為如果任務(wù)已經(jīng)完成了,但是自動取消仍需要1天時間,那么系統(tǒng)就會白白浪費資源在這1天上。

WithTimeout 與 WithDeadline 同理,只不過是 WithTimeout 用來接受一個過期時間,而不是接受一個過期時間節(jié)點:

func TestCtxWithTimeout(t *testing.T) {
	ctx := context.Background()
	ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
	defer cancel()
	go func() {
		select {
		case <-ctx.Done():
			fmt.Println("協(xié)程關(guān)閉", ctx.Err())
			return
		}
	}()

	time.Sleep(3 * time.Second)
}

// 結(jié)果
協(xié)程關(guān)閉 context deadline exceeded

WithValue 用來創(chuàng)建一個 valueCtx:

// 向上找到最近的上下文值
func TestCtxWithValue(t *testing.T) {
	ctx := context.Background()
	ctx1 := context.WithValue(ctx, "key", "ok")
	ctx2, _ := context.WithCancel(ctx1)
	// Value 會一直向上追溯到根節(jié)點,獲取當(dāng)前上下文攜帶的值,
	value := ctx2.Value("key")
	if value != nil {
		fmt.Println(value)
	}
}

// 結(jié)果
ok

這四個函數(shù)都是創(chuàng)建一個新的子節(jié)點,并不是直接修改當(dāng)前 ctx,所以最后生成的 ctx 鏈有可能是這樣的:

使用 ctx 退出 goroutine

回到開頭提到的那個例子,我們使用 context 對其改造一下:

func TestCtxCloseGoroutine(t *testing.T) {
	fmt.Printf("開始了,有%d個協(xié)程\n", runtime.NumGoroutine())

	ctx := context.Background()

	// 協(xié)程A
	go func(ctx context.Context) {
		ctx, cancel := context.WithCancel(ctx)
		// 協(xié)程B
		go func(ctx context.Context) {
			// 協(xié)程B1
			go func(ctx context.Context) {
				for {
					select {
					case <-ctx.Done():
						return
					default:
					}
				}
			}(ctx)
			// 協(xié)程B2
			go func(ctx context.Context) {
				// 協(xié)程C
				go func(ctx context.Context) {
					for {
						select {
						case <-ctx.Done():
							return
						default:
						}
					}
				}(ctx)
				for {
					select {
					case <-ctx.Done():
						return
					default:
					}
				}
			}(ctx)

			for {
				select {
				case <-ctx.Done():
					return
				default:
				}
			}
		}(ctx)

		// 1秒后通知退出
		time.Sleep(1 * time.Second)
		cancel()
		// A后續(xù)沒有任務(wù)了,會自動退出
	}(ctx)

	time.Sleep(2 * time.Second)
	fmt.Printf("最終結(jié)束,有%d個協(xié)程\n", runtime.NumGoroutine())
}

// 結(jié)果
開始了,有2個協(xié)程
最終結(jié)束,有2個協(xié)程

可以看到,和使用 channel 控制 goroutine 退出相比,context 大大降低了心智負擔(dān)。context 優(yōu)雅的實現(xiàn)了某一層任務(wù)退出,下層所有任務(wù)退出,上層任務(wù)和同層任務(wù)不受影響。

Go 語言最佳實踐:每次 context 的傳遞都應(yīng)該直接使用值傳遞,不應(yīng)該使用指針傳遞。這樣可以防止上下文的值被多個并發(fā)的 goroutine 修改而導(dǎo)致競爭問題。雖然使用值傳遞會導(dǎo)致一些微小的性能開銷,因為每次傳遞上下文時都需要復(fù)制一份數(shù)據(jù),但它提供了更好的并發(fā)安全性和程序可靠性。另外,由于上下文采用了值傳遞,也不應(yīng)該向上下文中存入較大的數(shù)據(jù),從而導(dǎo)致性能問題。

以上就是詳解Go語言中上下文context的理解與使用的詳細內(nèi)容,更多關(guān)于go上下文context的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Go設(shè)計模式之單例模式圖文詳解

    Go設(shè)計模式之單例模式圖文詳解

    單例模式是一種創(chuàng)建型設(shè)計模式,讓你能夠保證一個類只有一個實例,并提供一個訪問該實例的全局節(jié)點,本文就通過圖文給大家介紹一下Go的單例模式,需要的朋友可以參考下
    2023-07-07
  • golang環(huán)形隊列實現(xiàn)代碼示例

    golang環(huán)形隊列實現(xiàn)代碼示例

    這篇文章主要介紹了golang環(huán)形隊列實現(xiàn)代碼示例,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-11-11
  • Go語言struct要使用?tags的原因解析

    Go語言struct要使用?tags的原因解析

    這篇文章主要介紹了為什么?Go?語言?struct?要使用?tags,在本文中,我們將探討為什么 Go 語言中需要使用 struct tags,以及 struct tags 的使用場景和優(yōu)勢,需要的朋友可以參考下
    2023-03-03
  • Go語言中時間time相關(guān)處理方法詳解

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

    在Go語言中,time?包是處理時間和日期的核心,它提供了豐富的函數(shù)和方法,用于顯示、測量、計算、格式化、解析時間等,本文給大家詳細介紹了Go時間time相關(guān)處理方法的相關(guān)資料,需要的朋友可以參考下
    2024-10-10
  • golang利用redis和gin實現(xiàn)保存登錄狀態(tài)校驗登錄功能

    golang利用redis和gin實現(xiàn)保存登錄狀態(tài)校驗登錄功能

    這篇文章主要介紹了golang利用redis和gin實現(xiàn)保存登錄狀態(tài)校驗登錄功能,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧
    2024-01-01
  • Go語言數(shù)據(jù)結(jié)構(gòu)之二叉樹可視化詳解

    Go語言數(shù)據(jù)結(jié)構(gòu)之二叉樹可視化詳解

    這篇文章主要為大家詳細介紹了Go語言數(shù)據(jù)結(jié)構(gòu)中二叉樹可視化的方法詳解,文中的示例代碼講解詳細,感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2022-09-09
  • Go中的 panic / recover 簡介與實踐記錄

    Go中的 panic / recover 簡介與實踐記錄

    這篇文章主要介紹了Go中的 panic / recover 簡介與實踐,本文給大家介紹的非常詳細,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-04-04
  • Go語言獲取文件的名稱、前綴、后綴

    Go語言獲取文件的名稱、前綴、后綴

    這篇文章主要介紹了Go語言獲取文件的名稱、前綴、后綴,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2021-05-05
  • Go源碼分析之預(yù)分配slice內(nèi)存

    Go源碼分析之預(yù)分配slice內(nèi)存

    這篇文章主要從Go語言源碼帶大家分析一下預(yù)分配slice內(nèi)存的相關(guān)知識,文中的示例代碼簡潔易懂,對我們深入了解go有一定的幫助,需要的可以學(xué)習(xí)一下
    2023-08-08
  • Golang實現(xiàn)帶優(yōu)先級的select

    Golang實現(xiàn)帶優(yōu)先級的select

    這篇文章主要為大家詳細介紹了如何在Golang中實現(xiàn)帶優(yōu)先級的select,文中的示例代碼講解詳細,對我們學(xué)習(xí)Golang有一定的幫助,需要的可以參考一下
    2023-04-04

最新評論