「笔记007」Go语言Context初识 下

2024 年 11 月 29 日 星期五(已编辑)
11
摘要
本文介绍了Go语言的context在高级应用场景中的用法,包括多个协程共享context,结合http.Request的上下文管理,以及使用context实现超时重试。 - 在多协程中,父context的取消会影响所有子协程,并用sync.WaitGroup确保协程在主程序退出前完成。 - 在Web开发中,每个请求可用context管理,方便设置超时和传递数据。 - 使用context的Done方法进行任务的重试和超时控制。 注意事项包括避免滥用WithValue,确保资源释放,避免context跨边界传递。 最佳实践强调确定context范围、使用自定义类型作为键、明确取消信号,并避免将context作为函数返回值。
这篇文章上次修改于 2024 年 11 月 29 日 星期五,可能部分内容已经不适用,如有疑问可询问作者。

「笔记007」Go语言Context初识 下

5. 高级应用场景

5.1 多协程共享 context

context 的父子关系可以很好地控制多个协程的生命周期,尤其是在复杂任务中。

示例:任务分发与统一取消

package main

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

func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopped: %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())
    var wg sync.WaitGroup

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

    // 等待 2 秒后取消所有任务
    time.Sleep(2 * time.Second)
    cancel()

    // 等待所有协程结束
    wg.Wait()
    fmt.Println("All workers stopped.")
}

运行结果

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 stopped: context canceled
Worker 2 stopped: context canceled
Worker 3 stopped: context canceled
All workers stopped.

解析

  • context 的取消信号会级联到所有子协程。
  • 通过 sync.WaitGroup 确保协程在主程序退出前完成清理。

5.2 结合 http.Request 的上下文管理

在 Web 开发中,每个请求可以用一个 context 来管理。Go 的 net/http 包已经为每个请求默认附带了一个 context,开发者可以使用它来设置超时或传递数据。

示例:请求超时处理

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context() // 获取请求的 context

    fmt.Println("Handler started")
    defer fmt.Println("Handler ended")

    select {
    case <-time.After(2 * time.Second): // 模拟任务执行
        fmt.Fprintln(w, "Task completed")
    case <-ctx.Done(): // 处理取消信号
        fmt.Fprintln(w, "Request canceled:", ctx.Err())
    }
}

func main() {
    http.HandleFunc("/", handler)

    server := &http.Server{
        Addr: ":8080",
        Handler: http.TimeoutHandler(http.DefaultServeMux, 1*time.Second, "Timeout!"),
    }

    fmt.Println("Server is running on port 8080")
    server.ListenAndServe()
}

解析

  • 使用 http.RequestContext 方法可以获取请求的上下文。
  • 当客户端取消请求(如断开连接)或超时时,ctx.Done() 会关闭,触发取消逻辑。

5.3 使用 context 实现超时重试

当任务失败时,可能需要重试一定次数。在这种情况下,可以通过 context 实现重试和超时控制。

示例:带超时的任务重试

package main

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

func retryTask(ctx context.Context, attempts int, task func() error) error {
    for i := 0; i < attempts; i++ {
        select {
        case <-ctx.Done():
            return ctx.Err() // 如果超时或取消,直接返回
        default:
            if err := task(); err != nil {
                fmt.Printf("Attempt %d failed: %v\n", i+1, err)
            } else {
                return nil // 成功时退出
            }
        }
        time.Sleep(500 * time.Millisecond) // 重试间隔
    }
    return fmt.Errorf("all attempts failed")
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    err := retryTask(ctx, 5, func() error {
        fmt.Println("Performing task...")
        return fmt.Errorf("task failed")
    })
    if err != nil {
        fmt.Println("Task result:", err)
    }
}

解析

  • 通过 ctx.Done() 检查任务是否被取消。
  • 在重试逻辑中合理设置间隔时间,避免频繁重试。

6. 常见问题与注意事项

6.1 滥用 context

context 的设计初衷是用于传递控制信号(如取消、超时)和少量元数据,而非数据存储或函数通信工具。

反例:过度依赖 WithValue

ctx := context.WithValue(context.Background(), "userID", 12345)
// 在深层函数中读取
userID := ctx.Value("userID").(int)

问题

  • 滥用 WithValue 会导致代码耦合度高,难以维护。
  • 数据传递更应使用显式参数。

建议

  1. 只使用 WithValue 传递关键元数据(如请求 ID)。
  2. 对于大数据或复杂结构,使用专门的数据传递方式。

6.2 资源释放

当使用 context.WithCancelcontext.WithTimeout 时,务必调用 cancel() 释放资源。即使任务提前结束,未调用 cancel() 也可能导致内存泄露。

正确做法

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

6.3 避免跨边界传递 context

context 的生命周期应与任务保持一致,不应跨越函数或模块边界。例如,不要将 context 存储为全局变量或长期依赖。

问题

  • 传递过期的 context 会导致意外的取消或不可预测行为。
  • context 应始终是短生命周期的工具。

7. 最佳实践

7.1 确定 context 的范围

  • 短生命周期context 的生命周期应尽量与任务一致。
  • 适度嵌套:避免过深的 context 嵌套,确保逻辑清晰。

7.2 使用自定义类型作为键

避免键冲突,推荐使用自定义类型:

type ctxKey string
const UserIDKey ctxKey = "userID"
ctx := context.WithValue(context.Background(), UserIDKey, 12345)

7.3 明确取消信号

  • 在协程中始终监听 ctx.Done()
select {
case <-ctx.Done():
    return ctx.Err()
default:
    // 执行任务
}

7.4 context 不应作为函数返回值

context 的设计目的是在调用链中向下传递,而非作为返回值向上传递。


8. 结

  1. context 是 Go 并发模型的核心工具,用于任务取消、超时控制和上下文数据传递。
  2. 基本用法(WithCancelWithTimeout 等)并设计哲学(不可变性、父子结构)。
  3. 遵循最佳实践,避免滥用 WithValue 和全局 context,确保资源释放和逻辑清晰。

使用社交账号登录

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