Golang Context 的作用和用法

context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、共享数据等。

在 Go 语言程序中,关闭协程可以通过 channel+select 方式实现,而不是直接杀死协程。但是在某些场景下,例如某个请求衍生了很多协程,这些协程之间是相互关联,共享一些全局变量、有共同的生命周期,而且需要同时关闭,再用 channel+select 就会比较繁琐,而且有可能出现协程泄露问题。类似的场景,就可以通过 context 来实现。

context用途

其实 context 源码中也是通过 channel+select 来实现的,而且内部还构造了一棵派生关系树,便于生命周期、广播通知等管理,所以我们无需再造轮子。

context 使用起来非常方便。源码里对外提供了一个创建根节点 context 的函数:

func Background() Context

Background() 返回的是一个空的 Context,通常作为根节点,它没有任何功能,不能被取消,没有值,也没有超时时间。

有了根节点 Context,可以使用它作为参数,使用 context 包提供的四个函数创建子节点 Context:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

 

1. WithCancel

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel 函数的参数是父Context。

WithCancel 的返回值是父Context的副本 ctx 和一个取消函数 CancelFunc。

当返回的取消函数被调用时,或者父Context的 Done 通道被关闭时,返回的Context的 Done 通道将被关闭,顺序以最先发生的为准。

WithCancel 范例

使用 Context 的控制协程的简单范例。调用者 main 函数,启动运行一个协程 contextTest, 3秒后主动取消 contextTest 的运行。

package main

import (
	"context"
	"time"
)

func contextTest(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            // 被取消或者超时就结束协程
            println("goroutin finished")
            return
        default:
        }
        // 每隔 1 秒钟,打印 running
        time.Sleep(time.Second)
        println("running")
    }
}

func main() {
    // 返回值 cancelFunc 是一个函数,用于取消运行中的协程
    ctx, cancelFunc := context.WithCancel(context.Background())
    go contextTest(ctx)

    // 让contextTest 协程运行 3 秒钟,然后调用取消函数
    time.Sleep(3*time.Second)
    println("send a closed signal")
    cancelFunc()

    // 等待 3 秒钟,让 contextTest 协程优雅结束。
    time.Sleep(3*time.Second)
}

 

2. WithDeadline

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)

WithDeadline 函数的参数是父Context 和 截止时间 deadline。

WithDeadline 的返回值是父Context的副本 ctx 和一个取消函数 CancelFunc。

当协程运行到截止时间、返回的取消函数被调用,或者父Context的 Done 通道被关闭时,返回的Context的 Done 通道将被关闭。

WithDeadline 范例

使用 Context 的控制协程的简单范例。调用者 main 函数,启动运行一个协程 contextTest, 3秒后主动取消 contextTest 的运行。

package main

import (
    "context"
    "time"
)

func contextTest(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            // 被取消或者超时就结束协程
            println("goroutin finished")
            return
        default:
        }
        // 每隔 1 秒钟,打印 running
        time.Sleep(time.Second)
        println("running")
    }
}

func main() {
    // 3 秒后自动取消运行中的协程
    ctx, _ := context.WithDeadline(context.Background(),time.Now().Add(3 * time.Second))
    go contextTest(ctx)

    // 等待 5 秒钟,让 contextTest 协程优雅结束。
    time.Sleep(5*time.Second)
}

 

3. WithTimeout

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithTimeout 函数的参数是父Context 和 超时时间 timeout。

WithTimeout 的返回值是父Context的副本 ctx 和一个取消函数 CancelFunc。

当协程运行时间超过 timeout、返回的取消函数被调用,或者父Context的 Done 通道被关闭时,返回的Context的 Done 通道将被关闭。

WithTimeout 范例

使用 Context 的控制协程的简单范例。调用者 main 函数,启动运行一个协程 contextTest, 3 秒后主动取消 contextTest 的运行。

package main

import (
    "context"
    "time"
)

func contextTest(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            // 被取消或者超时就结束协程
            println("goroutin finished")
            return
        default:
        }
        // 每隔 1 秒钟,打印 running
        time.Sleep(time.Second)
        println("running")
    }
}

func main() {
    // 3 秒后自动取消运行中的协程
    ctx, _ := context.WithTimeout(context.Background(),3 * time.Second)
    go contextTest(ctx)

    // 等待 5 秒钟,让 contextTest 协程优雅结束。
    time.Sleep(5*time.Second)
}

 

4. WithValue

func WithValue(parent Context, key, val interface{}) Context

WithValue 函数的参数是父Context 和 key、val。key 和 val是一个键值对。

WithValue 的返回值是父Context的副本 ctx。

WithValue 仅对传递进程和api的请求范围内的数据使用上下文值,而不是将可选参数传递给函数。

提供的键必须是可比较的,不要使用字符串类型或任何其他内置类型,以避免使用上下文的包之间的冲突,使用者应该定义他们自己的键类型,通常为具体 struct{} 类型。或者,导出的上下文键变量的静态类型应该是一个指针或接口。

 

5. 使用 context 建议

Context 会在协程间传递,只需要在适当的时间调用 cancel 函数向 goroutines 发出取消信号或者调用 Value 函数取出 Context 中的值。

在官方博客里,对于使用 context 提出了几点建议:

  • 不要将 Context 塞到结构体里,而是直接将 Context 类型作为函数的第一参数,而且一般都命名为 ctx。
  • 不要向函数传入一个 nil 的 Context,如果你实在不知道传什么,标准库给你准备好了一个 context.TODO()。
  • 不要把本应该作为函数参数的类型塞到 Context Context 存储的应该是一些共同的数据。例如:登陆的 session、cookie 等。
  • 同一个 Context 可能会被传递到多个 goroutine,别担心,Context 是并发安全的。

context 用来在 goroutine 之间传递上下文信息,使用场景包括:取消 goroutine、传递共享的数据、防止 goroutine 泄漏等。 1. 取消 goroutine ...