「笔记006」Go语言Context初识 上

2024 年 11 月 29 日 星期五(已编辑)
10
摘要
在Go语言中,`context`模块主要用于解决协程管理中的难题,如优雅终止协程、复杂的超时控制和跨函数的数据传递问题。通过使用`context`可以传递取消信号、设定任务超时以及传递上下文数据,而不必修改函数签名。重要的函数包括`context.Background`(根context)用于初始化,`context.TODO`(占位context)用于开发阶段,`context.WithCancel`用于手动取消任务,`context.WithTimeout`和`context.WithDeadline`用于自动超时控制,`context.WithValue`用于传递元数据。但传递数据量要少,避免滥用。
这篇文章上次修改于 2024 年 11 月 29 日 星期五,可能部分内容已经不适用,如有疑问可询问作者。

「笔记006」Go语言Context初识 上

1. 为什么需要 context

1.1 背景问题

在 Go 中,协程(goroutine)是轻量级线程,可以轻松地启动数以千计的并发任务。然而,这种便利也带来了一些问题:

  1. 无法优雅地终止协程

    • 一旦一个协程启动,除非显式退出,否则它可能一直运行下去,导致资源泄露。
    • 如果一个任务失败了,依赖它的其他任务也需要取消,而传统的手段难以统一管理。
  2. 任务的超时控制复杂

    • 某些操作(如 HTTP 请求、数据库查询)需要超时控制,如果任务超时没有终止,可能会拖垮整个系统。
  3. 函数间依赖数据传递麻烦

    • 在多层函数调用中传递元数据(如用户身份、请求 ID)时,需要修改函数签名,这会增加代码的复杂性。

1.2 context 的设计目标

为了解决这些问题,Go 设计了 context,其目标包括:

  1. 取消信号传递:在协程树中优雅地传递取消信号。
  2. 超时控制:提供简单一致的超时处理方式。
  3. 上下文数据传递:避免修改函数签名的情况下传递元数据。

2. context 的基本概念

在 Go 中,context 是一个接口,定义了以下主要方法:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}

2.1 context.Backgroundcontext.TODO

这两个是创建 context 的入口点。

  • context.Background

    • 是一个顶层空 context,通常作为根 context 使用。
    • 常用于主函数、初始化逻辑或测试代码中。
    • 示例:

      ctx := context.Background()
  • context.TODO

    • 是一个临时占位的 context,表示还不确定要用什么类型的 context
    • 使用场景:
      1. 开发中的代码尚未决定使用何种 context
      2. 快速原型开发时的占位。

实际开发中,context.Background 是更常用的选择,context.TODO 主要用于过渡。


2.2 context.WithCancel

context.WithCancel 是创建可取消 context 的常用方法。

  • 用法

    ctx, cancel := context.WithCancel(parentCtx)
    defer cancel() // 确保资源释放
  • 原理

    • 返回一个子 context,它继承了父 context 的取消信号。
    • 调用 cancel() 会关闭 ctx.Done() 通道,并通知所有监听该通道的协程。
  • 示例

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go func(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("Worker canceled")
                return
            default:
                fmt.Println("Working...")
                time.Sleep(1 * time.Second)
            }
        }
    }(ctx)

    time.Sleep(3 * time.Second)
    cancel() // 取消任务
    time.Sleep(1 * time.Second)
}

运行结果:

Working...
Working...
Working...
Worker canceled

注意:如果不调用 cancel(),协程将一直运行,可能导致资源泄露。


2.3 context.WithTimeoutcontext.WithDeadline

这两者是用于设置任务超时的核心方法。

  • context.WithTimeout
    • 创建一个带有超时时间的 context,超时后自动取消。
    • 示例:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

select {
case <-time.After(3 * time.Second):
    fmt.Println("Task completed")
case <-ctx.Done():
    fmt.Println("Timeout:", ctx.Err())
}
  • context.WithDeadline
    • WithTimeout 类似,但通过设定截止时间控制超时。
    • 示例:
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2*time.Second))
defer cancel()

运行时,超时触发 ctx.Done() 通道关闭,并返回 context.DeadlineExceeded 错误。


2.4 context.WithValue

context.WithValue 用于在 context 中存储和传递数据。

  • 用法
ctx := context.WithValue(context.Background(), "userID", 12345)
value := ctx.Value("userID")
fmt.Println("User ID:", value)
  • 底层实现
    • context 并未使用 map 存储键值对,而是通过链表结构存储每个 key, value
    • 这种设计使得创建子 context 更高效。

注意事项

  1. 避免滥用 WithValue

    • 它仅适合传递少量的元数据(如用户身份、请求范围信息)。
    • 传递大量数据或复杂对象应使用其他方式。
  2. 使用自定义类型作为键

    • 为避免键冲突,推荐使用自定义类型:
type ctxKey string
const UserIDKey ctxKey = "userID"

3. 基础示例:协程控制

下面为 context 管理协程对例子:

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d canceled: %v\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Worker %d is working...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // 启动多个协程
    for i := 1; i <= 3; i++ {
        go worker(ctx, i)
    }

    // 等待一段时间后取消
    time.Sleep(2 * time.Second)
    cancel()
    time.Sleep(1 * time.Second) // 等待所有协程退出
}

运行结果

Worker 1 is working...
Worker 2 is working...
Worker 3 is working...
Worker 1 is working...
Worker 2 is working...
Worker 3 is working...
Worker 1 canceled: context canceled
Worker 2 canceled: context canceled
Worker 3 canceled: context canceled

4. 小结

  1. context.Background 是程序的根 context,推荐用作起点。
  2. context.WithCancel 用于显式取消任务,而 WithTimeoutWithDeadline 实现了自动超时控制。
  3. WithValue 用于传递轻量级的元数据,但应避免滥用。

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...