Golang 如何基于现有的 context 创建新的 context?

目录

[基于现有的 context 创建新的 context](#基于现有的 context 创建新的 context)

现有创建方法的问题

[Go 1.21 中的 context.WithoutCancel 函数](#Go 1.21 中的 context.WithoutCancel 函数)

[Go 版本低于 1.21 该怎么办?](#Go 版本低于 1.21 该怎么办?)


在 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 导致的报错了。

相关推荐
C++chaofan1 分钟前
JUC 并发编程从入门到精通(超详细笔记 + 实战案例)
java·jvm·spring boot·redis·后端·并发·juc
java porter2 分钟前
系统架构设计之单例模式(下)
开发语言·javascript·单例模式
小北方城市网4 分钟前
第 4 课:前端工程化进阶 ——Vue 核心语法 + 组件化开发(前端能力质的飞跃)
大数据·开发语言·数据库·python·状态模式·数据库架构
zhaokuner7 分钟前
02-通用语言与协作-DDD领域驱动设计
java·开发语言·设计模式·架构
㳺三才人子8 分钟前
初探 Python + Django
开发语言·python·django
IT_陈寒11 分钟前
Vite 5大实战优化技巧:让你的开发效率提升200%|2025前端工程化指南
前端·人工智能·后端
嵌入式×边缘AI:打怪升级日志13 分钟前
USB设备枚举过程详解:从插入到正常工作
开发语言·数据库·笔记
molaifeng20 分钟前
深度解密 Go 语言调度器:GMP 模型精讲
开发语言·golang·gmp
技术小泽21 分钟前
java转go速成入门笔记篇(一)
java·笔记·golang
资生算法程序员_畅想家_剑魔22 分钟前
Java常见技术分享-27-事务安全-事务日志-事务日志框架
java·开发语言