context上下文

wxvirus2022年8月3日
大约 3 分钟

引入

在 Go 的 http 包的server中,每一个请求都有一个对应的goroutine去处理,通常需要访问一些请求特定的数据,比如终端用户的身份认证信息、验证相关的 token、请求的截止时间,当一个请求被取消或超时时,所有用来处理请求的 goroutine 都应该迅速的退出,然后系统才能释放这些 goroutine 占用的资源。

为什么需要 Context

基本示例:如何优雅的退出 goroutine

使用全局变量方式

package main

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

var wg sync.WaitGroup
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(time.Second * 3)
	exit = true
	wg.Wait()
	fmt.Println("over")
}

问题

  1. 全局变量在跨包调用时不容易统一
  2. 如果worker中再启动 goroutine,就不好控制了

通道方式

package main

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

var wg sync.WaitGroup

func worker(exitChan chan struct{}) {
    // label 标签 搭配 goto break 比较适用多层嵌套退出
LOOP:
	for {
		fmt.Println("worker")
		time.Sleep(time.Second)
		select {
		case <-exitChan: // 等待接收上级通知
			break LOOP
		}
	}
	wg.Done()
}

func main() {
	var exitChan = make(chan struct{})
	wg.Add(1)
	go worker(exitChan)
	time.Sleep(time.Second * 3)
	// 给goroutine发送信号
	exitChan <- struct{}{}
	close(exitChan)
	wg.Wait()
	fmt.Println("over")
}

问题

  1. 使用全局变量在跨包调用时不容易实现规范和统一,需要维护一个共用的channel

实现目标

目标

如何在 goroutine 外部通知 goroutine 退出

  • 全局变量
  • 通道变量

上面 2 个都不是那么完美。Go1.7 之前都是程序员自己实现的

使用context

package main

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

var wg sync.WaitGroup

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:

		}
	}
}

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

Context 熟悉

它是用来专门简化对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。

context.Context是一个接口,该接口定义了四个需要实现的方法:

type Context interface {
	Deadline() (deadline time.Time, ok bool) // 截止时间
	Done() <-chan struct{}
	Err() error
	Value(key interface{}) interface{}
}
  • Deadline方法需要返回当前Context被取消的时间,也就是完成工作的截止时间
  • Done方法需要返回一个channel,这个channel会在当前工作完成或者上下文取消之后关闭,多次调用Done方法会返回同一个channel
  • Err方法会返回当前Context结束的原因,它只会在Done返回的channel被关闭时才会返回非空的值
    • 如果当前Context被取消就返回Canceled错误
    • 如果当前Context超时就会返回DeadlineExceeded错误
  • Value方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据

Background()和 TODO()

因为context.Context是一个接口,不能实例一个对象,只能使用实现接口的方法。所以就得借助这两种方法来生成context

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

func Background() Context {
	return background
}

func TODO() Context {
	return todo
}
context.Background() // emptyCtx != nil
context.TODO() // emptyCtx != nil
  • Background表示所有请求的context的顶层context,相当于很多个请求的”带头大哥“,主要用于main函数、初始化以及测试代码中,作为Context最顶层的Context
  • TODO:等下游接口,目前还不知道具体的使用场景,如果我们不知道该怎么使用 Context 的时候可以使用它

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

提示

想要从零开始创建Context,就可以借助这 2 个函数

Loading...