Go语言标准库:context包初识 | 豆包MarsCode AI刷题

95 阅读2分钟

背景与需求

在Go语言的http包中,每个请求都会由一个独立的goroutine处理,通常还会额外启动子goroutine来访问后端服务(如数据库或RPC)。这些goroutine需要共享与请求相关的数据(如认证信息、截止时间等)。当请求被取消或超时时,所有相关的goroutine都应迅速退出以释放资源。
为了解决这一问题,Go引入了context包,用于管理请求范围内的操作。


优雅退出goroutine的演变

1. 直接退出

此版本无法接收外部指令以控制goroutine的退出。

func worker() {
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
    }
    wg.Done()
}

func main() {
    wg.Add(1)
    go worker()
    wg.Wait()
    fmt.Println("over")
}

2. 全局变量控制退出

通过全局变量通知goroutine退出,但存在扩展性和维护性问题。

var exit bool

func worker() {
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        if exit {
            break
        }
    }
    wg.Done()
}

func main() {
    wg.Add(1)
    go worker()
    time.Sleep(3 * time.Second)
    exit = true
    wg.Wait()
    fmt.Println("over")
}

3. 通道方式

通过channel通知goroutine退出,相较全局变量更安全,但跨包调用时需额外维护全局的channel

func worker(exitChan chan struct{}) {
LOOP:
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        select {
        case <-exitChan:
            break LOOP
        default:
        }
    }
    wg.Done()
}

func main() {
    exitChan := make(chan struct{})
    wg.Add(1)
    go worker(exitChan)
    time.Sleep(3 * time.Second)
    exitChan <- struct{}{}
    close(exitChan)
    wg.Wait()
    fmt.Println("over")
}

官方推荐:Context的引入

context包从Go 1.7开始成为标准库,专为简化单个请求范围内的goroutine通信与取消操作而设计。以下是使用context的实现:

使用context.Context控制退出

func worker(ctx context.Context) {
LOOP:
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done(): // 监听取消信号
            break LOOP
        default:
        }
    }
    wg.Done()
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    wg.Add(1)
    go worker(ctx)
    time.Sleep(3 * time.Second)
    cancel() // 通知goroutine退出
    wg.Wait()
    fmt.Println("over")
}

如果子goroutine中还需要启动新的goroutine,只需继续传递ctx

func worker(ctx context.Context) {
    go worker2(ctx)
LOOP:
    for {
        fmt.Println("worker")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done():
            break LOOP
        default:
        }
    }
    wg.Done()
}

func worker2(ctx context.Context) {
LOOP:
    for {
        fmt.Println("worker2")
        time.Sleep(time.Second)
        select {
        case <-ctx.Done():
            break LOOP
        default:
        }
    }
}

Context接口与方法

context.Context是一个接口,主要方法包括:

  1. Deadline()
    返回Context的截止时间。
  2. Done()
    返回一个channel,用于通知goroutine结束。
  3. Err()
    返回Context结束的原因(如被取消或超时)。
  4. Value()
    用于存储请求范围内的数据,适合跨API传递信息。

Context的内置函数

1. Background()与TODO()

  • Background()
    通常作为顶层Context,适用于main函数、初始化或测试场景。
  • TODO()
    占位使用,当不知道该使用何种Context时调用。

2. With系列函数

  • WithCancel 创建可取消的Context:

    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()
    
  • WithDeadline 设置绝对超时时间:

    ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(3*time.Second))
    defer cancel()
    
  • WithTimeout 设置相对超时时间:

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    
  • WithValue 在Context中存储键值对:

    ctx := context.WithValue(context.Background(), "key", "value")
    fmt.Println(ctx.Value("key"))
    

使用Context的注意事项

  • 推荐以参数的方式显示传递Context
  • 以Context作为参数的函数方法,应该把Context作为第一个参数。
  • 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
  • Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
  • Context是线程安全的,可以放心的在多个goroutine中传递