Golang 的 context

context

简介

在 Go 语言中,context 可以用于在 goroutine 之间传递取消信号、设置截止时间以及携带请求范围的键值对数据的一种机制。
context 包提供了管理这些上下文对象的 API,使得服务端可以更好地处理诸如 HTTP 请求之类的长时间运行的任务,尤其是在涉及到并发处理的情况下。

Context 的基本用法

ctx := context.Background()

context.Background() 函数返回一个空的 Context 对象,通常用于在主函数中创建 Context 对象。

内置取消原因

内置的取消原因有:

  • context.Canceled
    取消
  • context.DeadlineExceeded
    超时

获取取消原因

  1. context.Cause
    触发取消时,返回预设的取消原因,若无则同 ctx.Err() , error 类型
  2. ctx.Err()
    返回取消原因,为 context 中预设,可作为错误内容判断的依据,error 类型

WithValue 携带键值对数据

上下文中携带键值对数据的方式,通过 context.WithValue() 函数实现。

  • context.WithValue()
    初始化一个携带键值对数据的 Context 对象。
  • ctx.Value()
    获取 Context 对象中的键值对数据。
// 上下文,携带内容
type Key string

func TestContextWithValue(t *testing.T) {
    kvs := map[string]string{
        "title":  "Fan的小破站",
        "domain": "fanfine.cn",
        "name":   "fan",
    }

    // 这里使用自定义类型是因为 key 的位置在使用内置类型时会出现提示:
    // should not use built-in type string as key for value; define your own type to avoid collisions (SA1029) go-staticcheck
    var key Key = "key"
    ctx := context.WithValue(context.Background(), key, kvs)

    fmt.Println(ctx.Value(key)) // map[domain:fanfine.cn name:fan title:Fan的小破站]

    kvs["other"] = "更多信息"
    fmt.Println(ctx.Value(key)) // map[domain:fanfine.cn name:fan other:更多信息 title:Fan的小破站]
}

WithValuevalue 可以是任何类型,包括指针、结构体、切片等。
可以使用引用类型,可以再创建 ctx 后续修改 value

模拟工作

func work(ctx context.Context) {

    for {
        select {
        case v, ok := <-ctx.Done():
            fmt.Println("结束时间", time.Now())
            fmt.Println("结束啦!Done!", "v:", v, "ok:", ok)
            return
        default:
            time.Sleep(time.Millisecond)
            fmt.Println("正在工作")
        }
    }

}

ctx.Done() 返回一个只读 chan, 仅读时会保持阻塞,
所以循环中会执行默认分支,模拟工作,直到接收到 chan 被关闭的信号
当没有取消信号时,ctx.Done() 会一直阻塞,直到被取消。
当 chan 关闭时,解除了阻塞,其可以读出 ok 为false
cancel() 取消会关闭 chan
Timeout Deadline 超时会关闭 chan

WithCancel 携带取消信号

  • context.WithCancel()
    返回一个带有取消信号的 Context 对象。
  • ctx.Done()
    获取取消信号的 channel,当取消信号发送时,channel 会被关闭。
  • cancel()
    发送取消信号,关闭 channel。
func TestContextCancel(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())

    go work(ctx)

    time.Sleep(time.Second * 2)
    cancel()
    fmt.Println(context.Cause(ctx)) // context canceled
    fmt.Println(ctx.Err())          // context canceled

    if ctx.Err() == context.Canceled {
        fmt.Println("确认是取消了")
    }
}

如果不使用 cancel()<- ctx.Done() 会一直阻塞,work() 不会结束。

那么 cancel() 后会发生什么呢?见下文

cancel 源码

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
// cancel sets c.cause to cause if this is the first time c is canceled.
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    if cause == nil {
        cause = err
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    c.cause = cause
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
        c.done.Store(closedchan)
    } else {
        close(d) // 关闭通道
    }
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err, cause)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c)
    }
}
  1. cancel() 函数首先判断是否已经取消,如果已经取消,则直接返回。
  2. 如果没有取消,则设置取消原因,并关闭通道。
  3. 遍历子上下文,递归调用 cancel() 函数,关闭子上下文。
  4. 释放资源。

所以 cancel() 后,这个上下文就没用了,如果后续还有使用的话,需要重新创建一个。

WithCancelCause 携带取消原因

返回一个携带预设取消原因的 Context 对象。
取消时可以传入错误原因。


// 携带取消原因的 context
func TestContextCancelCause(t *testing.T) {
    ctx, cancel := context.WithCancelCause(context.Background())

    go work(ctx)

    time.Sleep(time.Second * 2)

    cancel(errors.New("2秒已经过了,该结束了")) // 取消时设置原因
    fmt.Println(context.Cause(ctx))   // 为预设的错误:2秒已经过了,该结束了
    fmt.Println(ctx.Err())            // context canceled
}

WithTimeout 携带超时信号

上下文携带超时时间,达到超时时间,就会自动取消

  • context.WithTimeout()
    创建一个带有超时时间的 context
func TestContextWithTimeout(t *testing.T) {
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    go work(ctx)

    time.Sleep(time.Second * 2)                    // 等待2秒
    fmt.Println("记录超时原因:方式1:", context.Cause(ctx)) // context deadline exceeded
    fmt.Println("记录超时原因:方式2:", ctx.Err())          // context deadline exceeded
}
  1. 超时时间设置为 1 秒
  2. go work() 后睡眠2秒,制造超时条件

如此,会先超时,两种方法得到错误原因都是 上下文超过截止时间 context deadline exceeded

补充

context.WithTimeout() 函数内部调用了 context.WithDeadline()

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

WithTimeoutCause 携带超时原因

  • context.WithTimeoutCause()
    创建一个携带超时原因的 context
// 携带超时原因的超时
func TestContextWithTimeoutCause(t *testing.T) {
    ctx, cancel := context.WithTimeoutCause(context.Background(), time.Second, errors.New("执行任务超时啦"))
    defer cancel()

    go work(ctx)

    time.Sleep(time.Second * 2)                    // 等待2秒
    fmt.Println("记录超时原因:方式1:", context.Cause(ctx)) // 执行任务超时啦
    fmt.Println("记录超时原因:方式2:", ctx.Err())          // context deadline exceeded

}

使用 context.Cause() 可以获取到预设的超时原因
ctx.Err() 则是 context 的预设超时原因,为 context deadline exceeded

补充

context.WithTimeoutCause() 函数内部调用了 context.WithDeadlineCause()

func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc) {
    return WithDeadlineCause(parent, time.Now().Add(timeout), cause)
}

WithDeadline 携带截止时间

  • context.WithDeadline()
    创建一个携带截止时间的 context
func TestContextDeadline(t *testing.T) {
    ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Second))
    defer cancel()
    go work(ctx)

    time.Sleep(2 * time.Second)
    fmt.Println("记录超时原因:方式1:", context.Cause(ctx)) // context deadline exceeded
    fmt.Println("记录超时原因:方式2:", ctx.Err())          // context deadline exceeded
}

context.Cause() ctx.Err() 都是 context deadline exceeded

WithDeadlineCause 携带截止时间原因

  • context.WithDeadlineCause()
    创建一个有预设原因的,有截止时间的 context

func TestContextDeadlineCause(t *testing.T) {

    ctx, cancel := context.WithDeadlineCause(context.Background(), time.Now().Add(1*time.Second), errors.New("超过截止时间"))
    defer cancel()
    go work(ctx)

    time.Sleep(2 * time.Second)
    fmt.Println("记录超时原因:方式1:", context.Cause(ctx)) // 超过截止时间
    fmt.Println("记录超时原因:方式2:", ctx.Err())          // context deadline exceeded

}

使用 context.Cause() 可以获取到预设的超时原因
ctx.Err() 则是 context 的预设超时原因,为 context deadline exceeded

复合上下文

上述中 context 的初始化的方式,都是通过 context.Background() 来初始化的,
但是,也可以继承其他上下文。
只需在初始化新上的 context 时,传入一个父上下文即可。

比如:一个有超时时间(截止时间),且携带内容的上下文


// 上下文 - 继承其他上下文
func TestContextInherit(t *testing.T) {

    // 携带内容的上下文 ctx
    ctx := context.WithValue(context.Background(), "domain", "fanfine.cn")
    // ctx 继承自 ctx
    ctx, _ = context.WithTimeout(ctx, time.Second)
    go work(ctx)
    time.Sleep(time.Second * 2)
    fmt.Println("超时之后 从 ctx 中取出 domain 值:", ctx.Value("domain"))

    // 因为是紧随着上面超时的之后进行,其 ctx.Done() chan 已经关闭,
    // chan 关闭后不能重开,所以只能重新创建一个 ctx
    // ctx, _ = context.WithDeadline(ctx, time.Now().Add(time.Second*2))
    ctx = context.WithValue(context.Background(), "domain", "fanfine.cn")
    ctx, _ = context.WithDeadline(ctx, time.Now().Add(time.Second*2))
    go work(ctx)
    time.Sleep(time.Second * 4)
    fmt.Println("截止时间后 从 ctx 中取出 domain 值:", ctx.Value("domain"))

    /*  输出:

    (1063)正在工作
    结束时间 2024-09-18 05:24:39.392096 +0800 CST m=+1.001402793
    结束啦!Done! v: {} ok: false
    超时之后 从 ctx 中取出 domain 值: fanfine.cn

    (1063)正在工作
    结束时间 2024-09-18 05:24:42.3929 +0800 CST m=+4.002252293
    结束啦!Done! v: {} ok: false
    截止时间后 从 ctx 中取出 domain 值: fanfine.cn
    */
}

场景应用

在接口调用时使用context,可以设置超时时间。
超过时间后,中断接口调用,并返回错误。


// 应用场景,调用接口
func TestUrl(t *testing.T) {

    url := "https://example.com/api"

    ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
    defer cancel()

    // 初始化request
    buf := new(bytes.Buffer)
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, buf)
    if err != nil {
        panic("初始化请求失败")
    }
    req.Header.Set("Content-Type", "application/json")

    // 手动制造超时条件
    time.Sleep(2 * time.Second)

    // 执行请求
    client := http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("err:", err)
        if ctx.Err() == context.DeadlineExceeded {
            fmt.Println("请求超时,是的")
        } else {
            fmt.Println("请求失败:", err)
        }
        return
    }
    defer resp.Body.Close()

    // 读响应体
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("读取响应体失败:", err)
        return
    }

    fmt.Println("响应内容:", string(body))
}

本文由 上传。


如果您喜欢这篇文章,请点击链接 Golang 的 context 查看原文。


您也可以直接访问:https://www.fanfine.cn/blog/187

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇