详解Go如何基于现有的context创建新的context
作者:路多辛
在 Golang 中,context 包提供了创建和管理上下文的功能。当需要基于现有的 context.Context 创建新的 context 时,通常是为了添加额外的控制信息或为了满足特定的生命周期需求。
基于现有的 context 创建新的 context
可以基于现有的 context.Context 创建一个新的 context,对应的函数有 context.WithCancel、context.WithDeadline、context.WithTimeout 或 context.WithValue。这些函数会返回一个新的 context.Context 实例,继承了原来 context 的行为,并添加了新的行为或值。使用 context.WithValue 函数创建的简单示例代码如下:
package main import "context" func main() { // 假设已经有了一个context ctx ctx := context.Background() // 可以通过context.WithValue创建一个新的context key := "myKey" value := "myValue" newCtx := context.WithValue(ctx, key, value) // 现在newCtx包含了原始ctx的所有数据,加上新添加的键值对 }
使用 context.WithCancel 函数创建,简单示例代码如下:
package main import "context" func main() { // 假设已经有了一个context ctx ctx := context.Background() // 创建一个可取消的context newCtx, cancel := context.WithCancel(ctx) // 当完成了newCtx的使用,可以调用cancel来取消它 // 这将释放与该context相关的资源 defer cancel() }
现有创建方法的问题
先说一个使用场景:一个接口处理完基本的任务之后,后续一些处理的任务放使用新开的 Goroutine 来处理,这时候会基于当前的 context 创建一个 context(可以使用上面提到的方法来创建) 给 Goroutine 使用,也不需要控制 Goroutine 的超时时间。
这种场景下,Goroutine 的声明周期一般都会比这个接口的生命周期长,这就会出现一个问题——当前接口请求所属的 Goroutine 退出后会导致 context 被 cancel,进而导致新开的 Goroutine 中的 context 跟着被 cancel, 从而导致程序异常。看一个示例:
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() // 创建子 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 机制的创建函数。
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" }
原理其实很简单,主要功能是创建一个新的 context 类型,继承了父 context 的所有属性,但重写了 Deadline、Done、Err、Value 几个方法,当父 context 被取消时不会触发任何操作。
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 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() // 创建子 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() error Value(key any) any }
运行代码,在浏览器中访问 http://127.0.0.1:8080/test,发现不再报父 context 被 cancel 导致的报错了。
以上就是详解Go如何基于现有的context创建新的context的详细内容,更多关于Go context的资料请关注脚本之家其它相关文章!