26. Context 工具包

Go的Context工具包用于在单个请求的多个goroutine间共享数据、传递取消信号和截止时间。本文介绍了Context的结构,包括不可取消的背景Context(Background)和未知用途的TODO(TODO),以及如何通过context包的函数派生子Context。使用Context可以解决通过select+channel在复杂协程结构中控制结束的难题。遵循将Context作为参数传递,放在首位,不传递nil等原则,确保有效使用Context。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

What is context

Context 中文为 上下文,可以用来简化单个请求多个 goroutine 之间关于数据共享,取消信号,截止时间等操作。

Context 结构

Context 在 golang 工具包中,是一个 interface,其结构如下:

type Context interface {
  // 获取设置的截止时间的意思,第一个返回式是截止时间,到了这个时间点,Context会自动发起取消请求;第二个返回值ok==false时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。
	Deadline() (deadline time.Time, ok bool)

  // 返回一个只读的chan,类型为struct{},我们在goroutine中,如果该方法返回的chan可以读取,则意味着parent context已经发起了取消请求,我们通过Done方法收到这个信号后,就应该做清理操作,然后退出goroutine,释放资源。
	Done() <-chan struct{}

  // 返回取消的错误原因,因为什么Context被取消
	Err() error

  // 获取该Context上绑定的值,是一个键值对,所以要通过一个Key才可以获取对应的值,这个值一般是线程安全的。
	Value(key interface{}) interface{}
}

Context 接口并不需要我们实现,golang 内置了 2 个实现,可以把这 2 个内置的 context 作为顶层的 parent context,基于 parent context 继承派生 children context

var (
	background = new(emptyCtx)
	todo       = new(emptyCtx)
)

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}

一个是Background,主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。

一个是TODO,它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。

他们两个本质上都是emptyCtx结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。

Context 的继承派生

context 包提供了以下 with 函数来派生 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

Why use context

select + channel 协程间的通信

func communicateWithChannel(stop <-chan bool) {
	for {
		select {
		case <-stop:
			fmt.Println("children thread exiting...")
			return
		default:
			fmt.Println("children thread is doing task...")
			time.Sleep(2 * time.Second)
		}
	}
}

func TestChannel(t *testing.T) {
	stop := make(chan bool)

	go communicateWithChannel(stop)

	time.Sleep(time.Duration(10) * time.Second)

	stop <- true

	time.Sleep(time.Duration(10) * time.Second)
	fmt.Println("main thread is done")
}

通过 channel 虽然可以实现,父协程与子协程之间的通信,但是当 goroutine 中又开启新的 goroutine,形成树状结构时,便很难通过 channel 来控制子协程的结束。

How to use

func contextWithCancel(ctx context.Context) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("children thread exiting...")
			return
		default:
			fmt.Println("children thread is doing task...")
			time.Sleep(2 * time.Second)
		}
	}
}

func contextWithValue(ctx context.Context) {
	for {
		val := ctx.Value(ctxKeyName)
		select {
		case <-ctx.Done():
			fmt.Printf("children %d thread exiting...\n", val)
			return
		default:
			fmt.Printf("children %d thread is doing task...\n", val)
			time.Sleep(2 * time.Second)
		}
	}
}

func TestContextWithCancel(t *testing.T) {
	ctx, cancel := context.WithCancel(context.Background())

	go contextWithCancel(ctx)

	time.Sleep(time.Duration(10) * time.Second)

	cancel()

	time.Sleep(time.Duration(10) * time.Second)
	fmt.Println("main thread is done")
}

func TestContextWithValue(t *testing.T) {
	ctx, _ := context.WithTimeout(context.Background(), time.Duration(10)*time.Second)
	for i := 1; i <= 3; i++ {
		subCtx := context.WithValue(ctx, ctxKeyName, i)
		go contextWithValue(subCtx)
	}

	time.Sleep(time.Duration(15) * time.Second)
	fmt.Println("main thread is done")
}

Context 使用原则

  1. 不要把Context放在结构体中,要以参数的方式传递
  2. 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
  3. 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
  4. Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
  5. Context是线程安全的,可以放心的在多个goroutine中传递

Ref: https://www.flysnow.org/2017/05/12/go-in-action-go-context.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值