網站開發(fā)文檔包括長春網站建設方案咨詢
目錄
基于現有的 context 創(chuàng)建新的 context
現有創(chuàng)建方法的問題
Go 1.21 中的 context.WithoutCancel 函數
Go 版本低于 1.21 該怎么辦?
在 Golang 中,context 包提供了創(chuàng)建和管理上下文的功能。當需要基于現有的 context.Context 創(chuàng)建新的 context 時,通常是為了添加額外的控制信息或為了滿足特定的生命周期需求。
基于現有的 context 創(chuàng)建新的 context
可以基于現有的 context.Context 創(chuàng)建一個新的 context,對應的函數有 context.WithCancel、context.WithDeadline、context.WithTimeout 或 context.WithValue。這些函數會返回一個新的 context.Context 實例,繼承了原來 context 的行為,并添加了新的行為或值。使用 context.WithValue 函數創(chuàng)建的簡單示例代碼如下:
package mainimport "context"func main() {// 假設已經有了一個context ctxctx := context.Background()// 可以通過context.WithValue創(chuàng)建一個新的contextkey := "myKey"value := "myValue"newCtx := context.WithValue(ctx, key, value)// 現在newCtx包含了原始ctx的所有數據,加上新添加的鍵值對
}
使用 context.WithCancel 函數創(chuàng)建,簡單示例代碼如下:
package mainimport "context"func main() {// 假設已經有了一個context ctxctx := context.Background()// 創(chuàng)建一個可取消的contextnewCtx, cancel := context.WithCancel(ctx)// 當完成了newCtx的使用,可以調用cancel來取消它// 這將釋放與該context相關的資源defer cancel()
}
現有創(chuàng)建方法的問題
先說一個使用場景:一個接口處理完基本的任務之后,后續(xù)一些處理的任務放使用新開的 Goroutine 來處理,這時候會基于當前的 context 創(chuàng)建一個 context(可以使用上面提到的方法來創(chuàng)建) 給 Goroutine 使用,也不需要控制 Goroutine 的超時時間。
這種場景下,Goroutine 的聲明周期一般都會比這個接口的生命周期長,這就會出現一個問題——當前接口請求所屬的?Goroutine 退出后會導致 context 被 cancel,進而導致新開的 Goroutine 中的 context 跟著被 cancel, 從而導致程序異常??匆粋€示例:
package mainimport ("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 給新開的 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
}
運行代碼,在瀏覽器中訪問?http://127.0.0.1:8080/test,控制臺會打印如下錯誤信息:
goroutine...
Post "http://xxx.luduoxin.com/xxx": context canceled
可以看出,因為父級 context 被 cancel,導致子 context 也被 cancel,從而導致程序異常。因此,需要一種既能繼承父 context 所有的 value 信息,又能去除父級 context 的 cancel 機制的創(chuàng)建函數。
Go 1.21 中的 context.WithoutCancel 函數
這種函數該如何實現呢?其實 Golang 從 1.21 版本開始為我們提供了這樣一個函數,就是 context 包中的?WithoutCancel 函數。源代碼如下:
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"
}
原理其實很簡單,主要功能是創(chuàng)建一個新的 context 類型,繼承了父 context 的所有屬性,但重寫了 Deadline、Done、Err、Value 幾個方法,當父 context 被取消時不會觸發(fā)任何操作。
Go 版本低于 1.21 該怎么辦?
如果 Go 版本低于 1.21 其實也很好辦,按照 Go 1.21 中的實現方式自己實現一個就可以了,代碼可以進一步精簡,示例代碼如下:
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
}
使用自己實現的這個版本再跑一下之前的示例,代碼如下:
package mainimport ("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 給新開的 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() errorValue(key any) any
}
運行代碼,在瀏覽器中訪問 http://127.0.0.1:8080/test,發(fā)現不再報父 context 被 cancel 導致的報錯了。