Golang Context 上下文

Golang Context 上下文

1. Context 基本介绍

Go 1.7 标准库引入context,译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。

随着 context 包的引入,标准库中很多接口因此加上了 context 参数,例如database/sql包。context 几乎成为了并发控制和超时控制的标准做法

作用:
在一组 goroutine 之间传递共享的值、取消信号、deadline

在这里插入图片描述

2. Context 核心结构

context.Context是 Go 语言在 1.7 版本中引入标准库的接口,该接口定义了四个需要实现的方法:

type Context interface {
    // 返回被取消的时间
    Deadline() (deadline time.Time, ok bool)
    // 返回用于通知Context完结的channel
    // 当这个 channel 被关闭时,说明 context 被取消了
    // 在子协程里读这个 channel,除非被关闭,否则读不出来任何东西
    Done() <-chan struct{}
    // 返回Context取消的错误
    Err() error
    // 返回key对应的value
    Value(key any) any
}
Done() : 会关闭传递信号的 channel , 当 channel 被关闭了 , 就可以接收到数据了 ( context 被取消了 )

value() : 通过key:value 来传递context中需要共享的值
  • 除了 Context 接口,还存在一个 canceler 接口,用于实现 Context 可以被取消
type canceler interface {
    cancel(removeFromParent bool, err error)
    Done() <-chan struct{}
}

可以取消的上下文都应该实现 canceler 接口

  • 除了以上两个接口,还有4个预定义的Context类型:
// 空Context
type emptyCtx int

// 取消Context
type cancelCtx struct {
    Context
    mu       sync.Mutex            // protects following fields
    done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}

// 定时取消Context
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.

    deadline time.Time
}

// KV值Context
type valueCtx struct {
    Context
    key, val any
}

3. 默认 Context 使用

context 包中最常用的方法是context.Backgroundcontext.TODO,这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用:

  • context.Background是上下文的默认值,所有其他的上下文都应该从它衍生出来,在多数情况下,如果当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始的上下文向下传递。

  • context.TODO,是一个备用,一个 context 占位,通常用在并不知道传递什么 context的情形。

底层都是返回一个空context

// 创建方法
func Background() Context {
    return background
}
func TODO() Context {
    return todo
}

// 预定义变量
var (
    background = new(emptyCtx)
    todo       = new(emptyCtx)
)

// emptyCtx 定义
type emptyCtx int

func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
    return
}

func (*emptyCtx) Done() <-chan struct{} {
    return nil
}

func (*emptyCtx) Err() error {
    return nil
}

func (*emptyCtx) Value(key any) any {
    return nil
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

作用:

有些函数或者方法需要 context , 但是自己没有 context , 这时候就可以定义空 context

  • database/sql包中的某些函数
func (db *DB) PingContext(ctx context.Context) error
func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row
  • 具体使用
db, _ := sql.Open("", "")
query := "DELETE FROM `table_name` WHERE `id` = ?"
db.ExecContext(context.Background(), query, 42)

4. Contex 传递取消信号

4.1 主动取消

需要的操作为:

  1. 创建带有cancel函数的 Context,func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

  2. 接收 cancel 的 Channel,ctx.Done()

  3. 主动 Cancel 的函数,cancel CancelFunc

具有同一个 context 对象的 goroutine , 调用主动 Cancel 取消函数后 , ctx.Done()就可以取出数据

在这里插入图片描述

每一个 goroutine 都会监听c.Done() 这个 channel , 当监听到之后就会执行用户自己定义的操作 , 比如return 退出所有goroutine

取消这个操作不是由 context 执行的 , context 只负责去传递这个信号 , 有了这个信号 , 就可以基于这个信号做后续操作

func ContextCancel() {
	// 一:创建带有cancel函数的context
	ctx, cancel := context.WithCancel(context.Background())

	// 二: 启动goroutine,携带cancelCtx
	wg := sync.WaitGroup{}
	wg.Add(4)
	for i := 0; i < 4; i++ {
		go func(c context.Context, n int) {
			defer wg.Done()
			// 监听context的取消完成channel,来确定是否执行主动cancel操作
			fmt.Println("第", n, "个Goroutine")
			for {
				select {
				// 等待接收c.Done()这个channel
				case <-c.Done():
					fmt.Println("第", n, "个 Goroutine ", "context cancel")
					return
				default:

				}
				time.Sleep(300 * time.Millisecond)
			}
		}(ctx, i)
	}

	// 三: 定时取消cancel()
	// 定时器,三秒后取消所有goroutine的执行
	select {
	case <-time.NewTimer(3 * time.Second).C:
		fmt.Println("3秒时间到")
		cancel()
	}
	// 也可以使用select解决goroutine结束无法打印contex cancel问题
	select {
	case <-ctx.Done():
		fmt.Println("main context cancel")
	}
	wg.Wait()
}

4.2 Deadline 和 Timeout 定时取消

context.WithTimeout()某个时间段 , 比如5秒后

context.WithDeadline() 某个时间点 . 比如每天20:30

// 10s后cancel
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)

// 20:30 cancel
// time.Data()的方式
curr := time.Now()
t := time.Date(curr.Year(), curr.Month(), curr.Day(), 20, 30, 0, 0, time.Local)
ctx, cancel := context.WithDeadline(context.Background(), t)

// 以当前的时间加10分钟
context.WithDeadline(context.Background(),time.Now().Add( 10 * time.Minute))

带有时间的自动取消也可以自行调用 Cancel() 来实现主动取消

select {
	// 3秒后主动取消
	case <-time.NewTimer(3 * time.Second).C:
		cancel()
	// 通过withTimeout自动取消
	// context.WithTimeout(context.Background(), 10 * time.Second) 
	case ctx.Done() 
}

使用场景 : 不能确定主动调用是否能够调用成功 , 就可以使用但是取消

  • 从底层开始看出 , 定时取消是在主动取消的基础上增加的功能
type timerCtx struct {
	cancelCtx // 主动取消
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}
  • WithTimeout() 也是利用WithDeadline() 实现的
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

4.3 Cancel 操作的向下传递

当父上下文被取消时 , 子上下文也会被取消
在这里插入图片描述

func ContextExtends() {
	// 定义符合上图的context结构
	ContextOne, _ := context.WithCancel(context.Background())
	ContextTwo, cancel := context.WithCancel(ContextOne)
	ContextThree, _ := context.WithCancel(ContextOne)
	ContextFour, _ := context.WithCancel(ContextTwo)

	wg := sync.WaitGroup{}
	wg.Add(4)
	// 开启四个goroutine分别监控四个context的Done()方法
	go func(c context.Context) {
		defer wg.Done()
		select {
		case <-ContextOne.Done():
			fmt.Println("contextOne cancel")
		}
	}(ContextOne)
	go func(c context.Context) {
		defer wg.Done()
		select {
		case <-ContextTwo.Done():
			fmt.Println("contextTwo cancel")
		}
	}(ContextTwo)
	go func(c context.Context) {
		defer wg.Done()
		select {
		case <-ContextThree.Done():
			fmt.Println("contextThree cancel")
		}
	}(ContextThree)
	go func(c context.Context) {
		defer wg.Done()
		select {
		case <-ContextFour.Done():
			fmt.Println("contextFour cancel")
		}
	}(ContextFour)

	// 手动取消信号
	cancel()
	wg.Wait()
}

通过给对应 Context 执行 cancel , 输出遵循: 当父上下文被取消时 , 子上下文也会被取消

在这里插入图片描述
在这里插入图片描述

5. 取消操作流程源码

5.1 创建 cancelCtx 的流程

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    // 构建cancelCtx对象
    c := newCancelCtx(parent)
    // 传播Cancel操作
    propagateCancel(parent, &c)
    // 返回值,注意第二个cancel函数的实现
    return &c, func() { c.cancel(true, Canceled) }
}

func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{Context: parent}
}

核心操作:

  • newCancelCtx, 使用 parent 构建 cancelCtx
  • propagateCancel, 传播 Cancel 操作,用来构建父子 Context 的关联,用于保证在父级 Context 取消时可以同步取消子级 Context
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
    // parent不会触发cancel操作
    done := parent.Done()
    if done == nil {
        return // parent is never canceled
    }

    // parent已经触发了cancel操作
    select {
    case <-done:
        // parent is already canceled
        child.cancel(false, parent.Err())
        return
    default:
    }

    // parent还没有触发cancel操作
    if p, ok := parentCancelCtx(parent); ok {
        // 内置cancelCtx类型
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err)
        } else {
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            // 将当前context放入parent.children中
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        // 非内置cancelCtx类型
        atomic.AddInt32(&goroutines, +1)
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

以上代码在建立 child 和 parent 的 cancelCtx 联系时,处理了下面情况:

  • parent 不会触发 cancel 操作,不做任何操作,直接返回
  • parent 已经触发了 cancel 操作,执行 child 的 cancel 操作,返回
  • parent 还没有触发 cancel 操作,child 会被加入 parentchildren 列表中,等待 parent 释放取消信号
  • 如果是自定义 Context 实现了可用的 Done(),那么开启 goroutine 来监听parent.Done()child.Done(),同样在parent.Done()时取消 child

如果是 WithDeadline 构建的 timerCtx,构建的过程多了两步:

  • 对截至时间的判定,判定是否已经截至
  • 设置定时器
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    if parent == nil {
        panic("cannot create context from nil parent")
    }
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    propagateCancel(parent, c)

    dur := time.Until(d)
    // 已过时
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(false, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    // 设置定时器
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

5.2 ctx.Done() 初始信号 channel 流程

func (c *cancelCtx) Done() <-chan struct{} {
    // 加载已经存在的
    d := c.done.Load()
    if d != nil {
        return d.(chan struct{})
    }
    c.mu.Lock()
    defer c.mu.Unlock()

    // 初始化新的
    d = c.done.Load()
    if d == nil {
        d = make(chan struct{})
        c.done.Store(d)
    }
    return d.(chan struct{})
}

其中两个步骤:

  1. 先尝试加载已经存在的
  2. 后初始化新的

核心要点是,当调用Done()时,初始化chan struct{}, 而不是在上下文 cancelCtx 创建时,就初始化完成了。称为懒惰初始化

5.3 cancel() 操作流程

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    // 设置 err
    c.err = err
    // 关闭channel
    d, _ := c.done.Load().(chan struct{})
    if d == nil {
        c.done.Store(closedchan)
    } else {
        close(d)
    }
    // 遍历全部可取消的子context
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    // 从parent的children删除自己
    if removeFromParent {
        removeChild(c.Context, c)
    }
}

以上流程的核心操作:

  • 关闭 channel,用来通知全部使用该 ctx 的 goroutine
  • 遍历全部可取消的子 context,执行 child 的取消操作
  • 从 parent 的 children 删除自己

6. Context 传值

如果在 ContextB 中设置了一个值 , 那么这个值只会在 contextA 和基于A产生的 contextC 和 contextD 中使用

Web 开发中 , 每一个新请求创建一个 context 中 , 将数据存储到 context 中 , 当前 context 中的后续调用都可以用到该值
context 数据类型 : key - value 数据

在这里插入图片描述

func WithValue(parent Context, key, val any) Context

type Context interface {
    Value(key any) any
}

需要三个参数:

  • 上级 Context

  • key 要求是 comparable 的(可比较的),实操时,推荐使用特定的 Key 类型,避免直接使用 string 或其他内置类型而带来 package 之间的冲突。

// 避免冲突
// 例如其他包中有相同的字段"name"
type MyContextKey String
  • val any

6.1 单个 context 传值

type MyContext string
func ContextValue() {
	wg := sync.WaitGroup{}
	// 1.创建带有value的context
	ctx := context.WithValue(context.Background(), MyContextKey("name"), "Sakura")

	// 2.将ctx传入goroutine中
	wg.Add(1)
	key := "name"
	go func(c context.Context, key any) {
		defer wg.Done()
		// 通过key拿到value
		if value := c.Value(key); value != nil {
			fmt.Println("value:", value)
			return
		}
		fmt.Println("找不到key为\"", key, "\"的数据")

	}(ctx, key)

	wg.Wait()
}

要注意获取到的 value 为 any 空接口类型 , 如果想要使用 value 需要进行断言或者类型判断

  • 查看WithValue()可以返回返回值为valueCTX
func WithValue(parent Context, key, val any) Context {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	if key == nil {
		panic("nil key")
	}
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
	return &valueCtx{parent, key, val}
}
type valueCtx struct {
	Context
	key, val any
}

// valueCtx实现了这个value这个方法
func (c *valueCtx) Value(key any) any {
	if c.key == key {
		return c.val
	}
	return value(c.Context, key)
}

也就是除了 value 功能,其他 Contenxt 功能都由 parent Context 实现。

如果 context.valueCtx.Value 方法查询的 key 不存在于当前 valueCtx 中,就会从父上下文中查找该键对应的值直到某个父上下文中返回

6.2 多个context传值

type MyContext string
func ContextValue() {
	wg := sync.WaitGroup{}
	// 1.创建带有value的context
	ctxOne := context.WithValue(context.Background(), MyContext("name"), "Sakura1")
	ctxTwo := context.WithValue(ctxOne, MyContext("name"), "Sakura2")
	ctxThree := context.WithValue(ctxTwo, MyContext("name"), "Sakura3")

	// 2.将ctx传入goroutine中
	wg.Add(1)
	key := "name"
	go func(c context.Context, key any) {
		defer wg.Done()
		// 通过key拿到value
		if value := c.Value(key); value != nil {
			fmt.Println("value:", value)
			return
		}
		fmt.Println("找不到key为\"", key, "\"的数据")

	}(ctxThree, key)

	wg.Wait()
}

在这里插入图片描述

不存在会返回该 context 中上一级的 value

在这里插入图片描述

7. 使用 context 的注意事项

  1. 推荐以参数的方式显示传递 Context

  2. 以 Context 作为参数的函数方法,应该把 Context 作为第一个参数。

  3. 给一个函数方法传递 Context 的时候,不要传递 nil,如果不知道传递什么,就使用context.TODO()

  4. Context 的 Value 相关方法应该传递请求域的必要数据,不应该用于传递可选参数

  5. Context 是线程安全的,可以放心的在多个 goroutine 中传递

  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值