詳解Go如何基于現(xiàn)有的context創(chuàng)建新的context
在 Golang 中,context 包提供了創(chuàng)建和管理上下文的功能。當(dāng)需要基于現(xiàn)有的 context.Context 創(chuàng)建新的 context 時(shí),通常是為了添加額外的控制信息或?yàn)榱藵M足特定的生命周期需求。
基于現(xiàn)有的 context 創(chuàng)建新的 context
可以基于現(xiàn)有的 context.Context 創(chuàng)建一個(gè)新的 context,對(duì)應(yīng)的函數(shù)有 context.WithCancel、context.WithDeadline、context.WithTimeout 或 context.WithValue。這些函數(shù)會(huì)返回一個(gè)新的 context.Context 實(shí)例,繼承了原來(lái) context 的行為,并添加了新的行為或值。使用 context.WithValue 函數(shù)創(chuàng)建的簡(jiǎn)單示例代碼如下:
package main import "context" func main() { // 假設(shè)已經(jīng)有了一個(gè)context ctx ctx := context.Background() // 可以通過(guò)context.WithValue創(chuàng)建一個(gè)新的context key := "myKey" value := "myValue" newCtx := context.WithValue(ctx, key, value) // 現(xiàn)在newCtx包含了原始ctx的所有數(shù)據(jù),加上新添加的鍵值對(duì) }
使用 context.WithCancel 函數(shù)創(chuàng)建,簡(jiǎn)單示例代碼如下:
package main import "context" func main() { // 假設(shè)已經(jīng)有了一個(gè)context ctx ctx := context.Background() // 創(chuàng)建一個(gè)可取消的context newCtx, cancel := context.WithCancel(ctx) // 當(dāng)完成了newCtx的使用,可以調(diào)用cancel來(lái)取消它 // 這將釋放與該context相關(guān)的資源 defer cancel() }
現(xiàn)有創(chuàng)建方法的問(wèn)題
先說(shuō)一個(gè)使用場(chǎng)景:一個(gè)接口處理完基本的任務(wù)之后,后續(xù)一些處理的任務(wù)放使用新開(kāi)的 Goroutine 來(lái)處理,這時(shí)候會(huì)基于當(dāng)前的 context 創(chuàng)建一個(gè) context(可以使用上面提到的方法來(lái)創(chuàng)建) 給 Goroutine 使用,也不需要控制 Goroutine 的超時(shí)時(shí)間。
這種場(chǎng)景下,Goroutine 的聲明周期一般都會(huì)比這個(gè)接口的生命周期長(zhǎng),這就會(huì)出現(xiàn)一個(gè)問(wèn)題——當(dāng)前接口請(qǐng)求所屬的 Goroutine 退出后會(huì)導(dǎo)致 context 被 cancel,進(jìn)而導(dǎo)致新開(kāi)的 Goroutine 中的 context 跟著被 cancel, 從而導(dǎo)致程序異常。看一個(gè)示例:
package main import ( "bytes" "context" "errors" "fmt" "io" "net/http" "github.com/gin-gonic/gin" ) func main() { r := gin.New() r.GET("/test", func(c *gin.Context) { // 父 context,有使用取消功能 ctx, cancel := context.WithCancel(c) defer cancel() // 創(chuàng)建子 context 給新開(kāi)的 Goroutine 使用 ctxCopy, _ := context.WithCancel(ctx) go func() { err := TestPost(ctxCopy) fmt.Println(err) }() }) r.Run(":8080") } func TestPost(ctx context.Context) error { fmt.Println("goroutine...") buffer := bytes.NewBuffer([]byte(`{"xxx":"xxx"}`)) request, err := http.NewRequest("POST", "http://xxx.luduoxin.com/xxx", buffer) if err != nil { return err } request.Header.Set("Content-Type", "application/json") client := http.Client{} rsp, err := client.Do(request.WithContext(ctx)) if err != nil { return err } defer func() { _ = rsp.Body.Close() }() if rsp.StatusCode != http.StatusOK { return errors.New("response exception") } _, err = io.ReadAll(rsp.Body) if err != nil { return err } return nil }
運(yùn)行代碼,在瀏覽器中訪問(wèn) http://127.0.0.1:8080/test,控制臺(tái)會(huì)打印如下錯(cuò)誤信息:
goroutine...
Post "http://xxx.luduoxin.com/xxx": context canceled
可以看出,因?yàn)楦讣?jí) context 被 cancel,導(dǎo)致子 context 也被 cancel,從而導(dǎo)致程序異常。因此,需要一種既能繼承父 context 所有的 value 信息,又能去除父級(jí) context 的 cancel 機(jī)制的創(chuàng)建函數(shù)。
Go 1.21 中的 context.WithoutCancel 函數(shù)
這種函數(shù)該如何實(shí)現(xiàn)呢?其實(shí) Golang 從 1.21 版本開(kāi)始為我們提供了這樣一個(gè)函數(shù),就是 context 包中的 WithoutCancel 函數(shù)。源代碼如下:
func WithoutCancel(parent Context) Context { if parent == nil { panic("cannot create context from nil parent") } return withoutCancelCtx{parent} } type withoutCancelCtx struct { c Context } func (withoutCancelCtx) Deadline() (deadline time.Time, ok bool) { return } func (withoutCancelCtx) Done() <-chan struct{} { return nil } func (withoutCancelCtx) Err() error { return nil } func (c withoutCancelCtx) Value(key any) any { return value(c, key) } func (c withoutCancelCtx) String() string { return contextName(c.c) + ".WithoutCancel" }
原理其實(shí)很簡(jiǎn)單,主要功能是創(chuàng)建一個(gè)新的 context 類(lèi)型,繼承了父 context 的所有屬性,但重寫(xiě)了 Deadline、Done、Err、Value 幾個(gè)方法,當(dāng)父 context 被取消時(shí)不會(huì)觸發(fā)任何操作。
Go 版本低于 1.21 該怎么辦
如果 Go 版本低于 1.21 其實(shí)也很好辦,按照 Go 1.21 中的實(shí)現(xiàn)方式自己實(shí)現(xiàn)一個(gè)就可以了,代碼可以進(jìn)一步精簡(jiǎn),示例代碼如下:
func WithoutCancel(parent Context) Context { if parent == nil { panic("cannot create context from nil parent") } return withoutCancelCtx{parent} } type withoutCancelCtx struct { context.Context } func (withoutCancelCtx) Deadline() (deadline time.Time, ok bool) { return } func (withoutCancelCtx) Done() <-chan struct{} { return nil } func (withoutCancelCtx) Err() error { return nil }
使用自己實(shí)現(xiàn)的這個(gè)版本再跑一下之前的示例,代碼如下:
package main import ( "bytes" "context" "errors" "fmt" "io" "net/http" "time" "github.com/gin-gonic/gin" ) func main() { r := gin.New() r.GET("/test", func(c *gin.Context) { // 父 context,有使用取消功能 ctx, cancel := context.WithCancel(c) defer cancel() // 創(chuàng)建子 context 給新開(kāi)的 Goroutine 使用 ctxCopy := WithoutCancel(ctx) go func() { err := TestPost(ctxCopy) fmt.Println(err) }() }) r.Run(":8080") } func WithoutCancel(parent Context) Context { if parent == nil { panic("cannot create context from nil parent") } return withoutCancelCtx{parent} } type withoutCancelCtx struct { context.Context } func (withoutCancelCtx) Deadline() (deadline time.Time, ok bool) { return } func (withoutCancelCtx) Done() <-chan struct{} { return nil } func (withoutCancelCtx) Err() error { return nil } func TestPost(ctx context.Context) error { fmt.Println("goroutine...") buffer := bytes.NewBuffer([]byte(`{"xxx":"xxx"}`)) request, err := http.NewRequest("POST", "http://xxx.luduoxin.com/xxx", buffer) if err != nil { return err } request.Header.Set("Content-Type", "application/json") client := http.Client{} rsp, err := client.Do(request.WithContext(ctx)) if err != nil { return err } defer func() { _ = rsp.Body.Close() }() if rsp.StatusCode != http.StatusOK { return errors.New("response exception") } _, err = io.ReadAll(rsp.Body) if err != nil { return err } return nil } type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any }
運(yùn)行代碼,在瀏覽器中訪問(wèn) http://127.0.0.1:8080/test,發(fā)現(xiàn)不再報(bào)父 context 被 cancel 導(dǎo)致的報(bào)錯(cuò)了。
以上就是詳解Go如何基于現(xiàn)有的context創(chuàng)建新的context的詳細(xì)內(nèi)容,更多關(guān)于Go context的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang實(shí)現(xiàn)Directional Channel(定向通道)
這篇文章主要介紹了Golang實(shí)現(xiàn)Directional Channel(定向通道),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02Go語(yǔ)言調(diào)用ffmpeg-api實(shí)現(xiàn)音頻重采樣
最近對(duì)golang處理音視頻很感興趣,對(duì)golang音視頻常用庫(kù)goav進(jìn)行了一番研究。自己寫(xiě)了一個(gè)wav轉(zhuǎn)采樣率的功能。給大家分享一下,中間遇到了不少坑,解決的過(guò)程中還是蠻有意思的,希望大家能喜歡2022-12-12Go語(yǔ)言通過(guò)http抓取網(wǎng)頁(yè)的方法
這篇文章主要介紹了Go語(yǔ)言通過(guò)http抓取網(wǎng)頁(yè)的方法,實(shí)例分析了Go語(yǔ)言通過(guò)http操作頁(yè)面的技巧,需要的朋友可以參考下2015-03-03Go高效率開(kāi)發(fā)Web參數(shù)校驗(yàn)三種方式實(shí)例
這篇文章主要介紹了Go高效率開(kāi)發(fā)Web參數(shù)校驗(yàn)三種方式實(shí)例,需要的朋友可以參考下2022-11-11Gin+Gorm實(shí)現(xiàn)增刪改查的示例代碼
本文介紹了如何使用Gin和Gorm框架實(shí)現(xiàn)一個(gè)簡(jiǎn)單的增刪改查(CRUD)示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2024-12-12