Go与Context

Friday, May 6, 2022

1. 简介

首先,先看一下 Golang 博客上关于 Context 的描述,点击查看原文

在 Go 服务中,每一个请求都是在它自己的 Goroutine 中处理,每一个处理通常又会启动额外的 Goroutine 来访问后端服务,如数据库或RPC服务等,处理请求的 Goroutine 集通常需要访问特定于请求的值,例如最终用户的身份、授权令牌和请求的截止日期。当一个请求被取消或者超时,所有处理该请求的 Goroutine 都应该快速的退出,以便系统可以回收它们正在使用的任何资源。

在 Google,我们开发了一个 context 包,可以轻松地将请求范围的值、取消信号和截止日期跨API边界传给处理请求所涉及的所有 Goroutine,这个包以 context 包公开可用。

简单的总结,context 包提供了上下文,可以携带值、取消信号以及截止日期。

官方文档中对 Context 的使用有一些说明和建议:

  1. 传入请求应当创建一个 Context,对服务的传出调用应该接收一个 Context

  2. 函数调用链之间必须传递 Context,可使用 WithDeadlineWithTimeoutWithCancelWithValue 创建的修改副本

  3. 不要将 Context 存储在结构类型中,应当显示传递给需要它的每一个函数

  4. Context 应该是第一个参数,通常命名为 ctx

  5. 不要传递 nil Context,即便函数允许,如果不确定需要传递哪个 Context,请传递 context.TODO

  6. 仅将上下文值用于传递进程或Api进程间的请求范围数据,不要用于给函数传递可选参数

  7. 相同的 Context 可以传递给不同 Goroutine 中运行的函数,Context 在多个 Goroutine 中使用是安全的

2. 源码

2.1 变量

context 包定义了两个变量:

  • context.Canceled:当上下文被取消时返回的错误

    var Canceled = errors.New("context canceled")
    
  • context.DeadlineExceeded: 当上下文的最后期限已过时返回的错误

    var DeadlineExceeded error = deadlineExceededError{}
    
    type deadlineExceededError struct{}
    
    func (deadlineExceededError) Error() string   { return "context deadline exceeded" }
    func (deadlineExceededError) Timeout() bool   { return true }
    func (deadlineExceededError) Temporary() bool { return true }
    

2.2 类型

2.2.1 Context

context 包的核心是 Context 接口,它仅包含四个函数,如下:

type Context interface {
  
	Deadline() (deadline time.Time, ok bool)
  
	Done() <-chan struct{}
  
	Err() error
  
	Value(key interface{}) interface{}
}
  1. Deadline() (deadline time.Time, ok bool)

    Deadline 返回代表此上下文完成的工作应该被取消的时间,没有设置截止日期的时候,截止日期返回 ok == false,对 Deadline 的连续调用返回相同的结果。

  2. Done() <-chan struct{}

    当代表此上下文完成的工作应该被取消时,Done 返回一个关闭的通道,如果这个上下文永远不会被关闭,Done 可能会返回 nil。

    对Done 的连续调用会返回相同的结果,Done 通道的关闭有可能会异步发生,在取消函数返回之后。

    Done 通道关闭的时机:

    • WithCancel: 调用 cancel 时关闭
    • WithDeadline: 截止日期到期时关闭
    • WithTimeout: 超时结束时关闭

    Done 用于在 select 语句中使用,如下:

    // DoSomething 逻辑处理
    func DoSomething(ctx context.Context) (int, error) {
    	// 此处忽略逻辑
    	return 0, nil
    }
    
    // Demo 示例
    func Demo(ctx context.Context, out chan<- int) error {
    	for {
    		v, err := DoSomething(ctx)
    		if err != nil {
    			return err
    		}
    		select {
    		case <-ctx.Done():
    			return ctx.Err()
    		case out <- v:
    		}
    	}
    }
    

    Demo 函数循环调用 DoSomething 方法,如果 DoSomething 返回错误或者 ctx.Done 是关闭的话,则返回错误。

  3. Err() error

    如果 Done 没有被关闭,则 Err 返回 nil,否则返回一个用于说明为什么的非 nil 错误:

    • context.Canceled:上下文被关闭

    • context.DeadlineExceeded:上下文的最后期限已过

    在 Err 返回非 nil 错误后,对 Err 的连续调用将会返回相同的错误。

  4. Value(key interface{}) interface{}

    Value 返回上下文中与 key 关联的值,如果没有则返回 nil,使用相同的 key 连续调用将返回相同的结果。

    仅将上下文值用于传递进程或Api进程间的请求范围数据,不要用于给函数传递可选参数。

    key 标识上下文中的特定值,希望在 Context 中存储值的函数通常会在全局变量中分配一个 key,然后将这个 key 用于 context.WithValue 以及 Context.Value

    key 可以定义为任何类型,包应该将 key 定义为未导出类型以避免冲突。

    定义上下文 key 的包应该为使用该 key 的值定义一个类型安全的访问器,如下:

    package demo
    
    import (
    	"context"
    	"time"
    )
    
    // Token 凭证
    type Token struct {
    	value    string
    	deadline time.Time
    	userID   int64
    }
    
    // key key 是该包中定义的为导出的类型,这样可以避免与其它包冲突
    type key int
    
    // tokenKey 上下文中 demo.Token 关联的 key,也是未导出的
    var tokenKey key
    
    // NewContext 返回 demo 包的上下文,该上下文携带了 token
    func NewContext(ctx context.Context, token *Token) context.Context {
    	return context.WithValue(ctx, tokenKey, token)
    }
    
    // FromContext 从上下文中取值
    func FromContext(ctx context.Context) (*Token, bool) {
    	v, ok := ctx.Value(tokenKey).(*Token)
    	return v, ok
    }
    

    以上示例代码来自于官方的说明,通过在保内定义指定 key,并且限定为不导出,再通过 NewContextFromContext 方法为该 key 提供了安全的访问方式。

2.2.2 CancelFunc

CancelFunc 告诉操作放弃它的工作,它不会等待工作停止,一个 CancelFunc 可能会被多个 Goroutine 同时调用,当第一次调用后,后续对它的调用将什么也不会做。

2.2.3 emptyCtx

空上下文没有永远不会被取消,没有值,没有截止日期,它也不是结构体 struct{},因为这种类型的变量必须有不同的地址。

emptyCtx 实现了 Context 接口 和 Stringer 接口,Background 和 TODO 类型的上下文都是 emptyCtx。

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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"
}

2.2.4 canceler

canceler 是一种上下文类型,可以直接被取消,有两种实现:*cancelCtx 和 *timerCtx

// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
	cancel(removeFromParent bool, err error)
	Done() <-chan struct{}
}
  • cancel:取消方法,可以指定是否从父级上下文中移除
  • Done:返回一个关闭的通道

2.2.5 cancelCtx

cancelCtx 可以被取消,当被取消的时候,它还会取消任何实现 cancel 的子级.

cancelCtx 的几个成员说明:

  • mu:互斥锁,保护下面几个成员
  • done:done 通道,惰性创建(调用 Done() 时创建),第一次调用 cancel 方法的时候关闭
  • children:子级,第一次调用 cancel 方法的时候设置为 nil
  • err:错误,第一次调用 cancel 方法的时候设置为非 nil
// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
	Context

	mu       sync.Mutex            // protects following fields
	done     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
}

// &cancelCtxKey is the key that a cancelCtx returns itself for.
var cancelCtxKey int

func (c *cancelCtx) Value(key interface{}) interface{} {
	if key == &cancelCtxKey {
		return c
	}
	return c.Context.Value(key)
}

func (c *cancelCtx) Done() <-chan struct{} {
	c.mu.Lock()
	if c.done == nil {
		c.done = make(chan struct{})
	}
	d := c.done
	c.mu.Unlock()
	return d
}

func (c *cancelCtx) Err() error {
	c.mu.Lock()
	err := c.err
	c.mu.Unlock()
	return err
}

func (c *cancelCtx) String() string {
	return contextName(c.Context) + ".WithCancel"
}

// closedchan is a reusable closed channel.
var closedchan = make(chan struct{})

func init() {
	close(closedchan)
}

// 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) {
  // 如果传入的错误为空,则返回 panic
	if err == nil {
		panic("context: internal error: missing cancel error")
	}
  // 如果上下文的错误不为空,说明已经取消过,直接返回即可
	c.mu.Lock()
	if c.err != nil {
		c.mu.Unlock()
		return // already canceled
	}
  // 将错误赋值到上下文
	c.err = err
  // 由于 done 是在调用 Done 的时候才创建,所以此处 done 有可能为 nil
  // 如果是 nil,则将 done 赋值为一个已经关闭的 channel
  // 如果不是 nil,则关闭这个 channel
	if c.done == nil {
		c.done = closedchan
	} else {
		close(c.done)
	}
  // 依次取消子级
	for child := range c.children {
		// NOTE: acquiring the child's lock while holding parent's lock.
		child.cancel(false, err)
	}
  // 清除子级 map
	c.children = nil
	c.mu.Unlock()
  
	// 如果需要从父级移除,则调用 removeChild 从父级中清理掉自己
	if removeFromParent {
		removeChild(c.Context, c)
	}
}

2.2.6 timerCtx

timerCtx 携带了一个 timer 和一个截止时间,嵌入了一个 cancelCtx 来实现 Done 和 Err,通过停止计时器然后委托 cancelCtx 来实现 cancel。

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
	cancelCtx
	timer *time.Timer // Under cancelCtx.mu.

	deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
	return c.deadline, true
}

func (c *timerCtx) String() string {
	return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
		c.deadline.String() + " [" +
		time.Until(c.deadline).String() + "])"
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
  // 调用 cancelCtx 的 cancel
	c.cancelCtx.cancel(false, err)
	if removeFromParent {
		// Remove this timerCtx from its parent cancelCtx's children.
		removeChild(c.cancelCtx.Context, c)
	}
	c.mu.Lock()
  // 如果 timer 不为 nil,则停止计时器
	if c.timer != nil {
		c.timer.Stop()
		c.timer = nil
	}
	c.mu.Unlock()
}

2.2.7 valueCtx

valueCtx 会携带一个键值对,逻辑简单,实现了 Value 函数。

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
	Context
	key, val interface{}
}

func (c *valueCtx) String() string {
	return contextName(c.Context) + ".WithValue(type " +
		reflectlite.TypeOf(c.key).String() +
		", val " + stringify(c.val) + ")"
}

func (c *valueCtx) Value(key interface{}) interface{} {
  // 如果传入的 key 与上下文的 key 相同,则返回值
	if c.key == key {
		return c.val
	}
  // 如果不相同,则从父级查询
	return c.Context.Value(key)
}

2.3 函数

2.3.1 func Background() Context

Background 返回了一个非 nil 的空上下文,它永远不会被取消,没有存储值也没有最后期限,通常由主函数、初始化及测试使用,并且作为请求的顶级上下文使用。

Background 本质上是 emptyCtx。

var (
	background = new(emptyCtx)
)

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
	return background
}

2.3.2 func TODO() Context

TODO 返回一个非 nil 的空上下文,当不清楚需要使用何种 Context 或者它尚不可用时(周边函数尚未扩展为接收 Context 参数)。TODO 会被静态分析工具识别,这些工具确定上下文是否在程序中正确传播。

TODO 本质上是 emptyCtx。

var (
	todo       = new(emptyCtx)
)

// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
	return todo
}

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

WithValue 返回了一个基于父级并携带一个键值对的副本,需要注意几点:

  1. 仅将上下文值用于传递进程或Api进程间的请求范围数据,不要用于给函数传递可选参数
  2. key 需要是可以比较的类型,并且不应该是字符串类型或任何内置类型
  3. 使用 WithValue 应该定义自己的 key 类型
  4. 为避免在分配给 interface{} 时进行分配,上下文键通常具有具体类型 struct{}
  5. 导出的上下文键变量的静态类型应该是指针或接口。
// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) Context {
  // 检测父级
	if parent == nil {
		panic("cannot create context from nil parent")
	}
  // 检测key
	if key == nil {
		panic("nil key")
	}
  // 检测key是否是可比较类型
	if !reflectlite.TypeOf(key).Comparable() {
		panic("key is not comparable")
	}
  // 基于父级创建 valueCtx
	return &valueCtx{parent, key, val}
}

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

WithCancel 返回一个基于父级上下文并携带一个新的 Done 通道的上下文,当返回的取消函数被调用或者父级的 Done 通道被关闭时,返回的 Done 通道会被关闭,这两种情景以先发生者为准。

取消上下文会释放与其关联的资源,因此代码应在上下文执行的操作完成后立即调用取消方法。

// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
	c := newCancelCtx(parent)
	propagateCancel(parent, &c)
	return &c, func() { c.cancel(true, Canceled) }
}

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
	return cancelCtx{Context: parent}
}

// propagateCancel arranges for child to be canceled when parent is.
// 传播取消,负责保证在父级取消的时候,可以正确传播到子级
func propagateCancel(parent Context, child canceler) {
  // 此处的 parent 可以是任意类型的上下文,如果是 emptyCtx 类型,则 parent.Done() 返回的永远是 nil
  // 如果是 cancelCtx 或者 timerCtx,返回的不会是 nil
  // 如果不为 nil,说明永远不会取消,也就不需要一层一层通知子级取消
	done := parent.Done()
	if done == nil {
		return // parent is never canceled
	}

  // 判断 done channel 是否可以读取,可以读取则表明父级已取消,则需要取消子级
	select {
	case <-done:
		// parent is already canceled
		child.cancel(false, parent.Err())
		return
	default:
	}

  // 获取父级的底层cancelCtx
	if p, ok := parentCancelCtx(parent); ok {
    // 如果获取到,则进行取消
		p.mu.Lock()
		if p.err != nil {
			// parent has already been canceled
      // 如果父级已经取消,则直接取消子级即可
			child.cancel(false, p.err)
		} else {
      // 父级没有取消
      // 如果子级为 nil,则需要在父级的 children 里关联子级
      // 这样当父级取消的时候,会依次取消父级 children 里面关联的所有子级
			if p.children == nil {
				p.children = make(map[canceler]struct{})
			}
			p.children[child] = struct{}{}
		}
		p.mu.Unlock()
	} else {
    // 这里有多种情况:
    // 	1. 父级已取消
    //  2. 父级永远不会取消
    //  3. 无法转换为标准 cancelCtx
    //  4. 是一个自定义 Done 的 cancelCtx
    // 前两种情况在函数开头已经判断处理,此处无需处理
    // 后两种情况无法通过父级的 children map 来取消,因此启动一个 Goroutine 来处理
		atomic.AddInt32(&goroutines, +1)
		go func() {
			select {
			case <-parent.Done():
				child.cancel(false, parent.Err())
			case <-child.Done():
			}
		}()
	}
}

// parentCancelCtx returns the underlying *cancelCtx for parent.
// It does this by looking up parent.Value(&cancelCtxKey) to find
// the innermost enclosing *cancelCtx and then checking whether
// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
// has been wrapped in a custom implementation providing a
// different done channel, in which case we should not bypass it.)
// 获取父级的底层cancelCtx
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
  // 如果父级的 done channel 为空,说明不支持 cancel
  // 如果父级的 done channel 为 closedchan,说明父级已经取消
	done := parent.Done()
	if done == closedchan || done == nil {
		return nil, false
	}
  // 判断父级是否可以转换为 cancelCtx,如果无法转为,则返回 nil 和 false
	p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
	if !ok {
		return nil, false
	}
  // 如果可以转换,说明父级是 cancelCtx 类型或其衍生出来的上下文,比如 timerCtx 或者用户自定义的 cancelCtx
  // 需要进一步判断 父级与提取出来的上下文的 done channel 是否一致
  // 如果一致,说明就是标准的 cancelCtx 或 timerCtx,或自定义 cancelCtx 但没有重写 Done()
  // 如果不一致,说明是自定义的 cancelCtx 并且 重写了 Done(),这种情况需要单独处理
	p.mu.Lock()
	ok = p.done == done
	p.mu.Unlock()
	if !ok {
		return nil, false
	}
	return p, true
}

从以上代码可以看出,WithCancel 不能基于为 nil 的上下文来创建,创建的时候会先生成一个 cancelCtx,之后调用 propagateCancel 方法来传播取消。

propagateCancel 函数的目的是传播取消,它的大致流程如下:

  1. 判断父级是否会取消,如果不会取消则不执行任何操作
  2. 如果父级可取消,则读取父级的 done channel,如果取到说明父级已取消,此时需要取消对应传入的子级
  3. 如果父级当前未取消,则需要从父级提取底层的 cancelCtx,观察是否是标准的 cancelCtx 或者是自定义但没有覆盖 Done() 方法,如果是,则判断父级是否取消,如果取消则取消传入的子级;如果没有取消,则将传入的子级加入父级的 children,这样当父级取消的时候也会自动取消子级
  4. 如果父级底层是自定义 Done 的 cancelCtx,则启动 Goroutine 来完成取消操作

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

WithDeadline 返回基于父级的副本,其最后期限调整为不迟于传入的时间,如果父级的截止日期早于传入的时间,本质上是返回一个 cancelCtx。

上下文的 done channel 会在三种情况下关闭,这三种情况以先发生者为准:

  1. 到了截止时间
  2. 父级的 done channel 关闭
  3. 返回的取消函数被调用
// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
	if parent == nil {
		panic("cannot create context from nil parent")
	}
  // 判断父级是否支持 Deadline,如果支持并且截止时间早于传入的时间,意味着父级一定会在当前要创建的上下文前取消
  // 这样,当父级被取消的时候,其关联的子级也会自动被取消
 	// 所以此处返回一个 cancelCtx 就可以了
	if cur, ok := parent.Deadline(); ok && cur.Before(d) {
		// The current deadline is already sooner than the new one.
		return WithCancel(parent)
	}
  // 创建一个 timerCtx
	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) }
	}
  // 当前时间未到截止时间,则创建一个定时器,在定时器到时间收自动 cancel
	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) }
}

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

WithTimeout 本质上是调用的 WithTimeout,是在当前时间上指定时间范围来确定截止时间。

// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// 	func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// 		ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// 		defer cancel()  // releases resources if slowOperation completes before timeout elapses
// 		return slowOperation(ctx)
// 	}
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
	return WithDeadline(parent, time.Now().Add(timeout))
}

3. 示例

3.1 WithCancel

这个例子中,通过 WithCancel 获得一个 cancelCtx,之后启动协程,每隔一秒打印一句 hahaha,直到收到取消信号,主协程中,会在10秒后调用 cancel 函数。

package main

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

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	go Test(ctx)
	time.Sleep(10 * time.Second)
	cancel()
	time.Sleep(1 * time.Second)
}

func Test(ctx context.Context) {
	for range time.Tick(time.Second) {
		select {
		case <-ctx.Done():
			log("context is canceled")
			return
		default:
			log("hahaha")
		}
	}
}

func log(msgs ...interface{}) {
	fmt.Println(time.Now().Format("2006-01-02 15:04:05"), msgs)
}

执行结果如下:

2022-05-07 10:02:33 [hahaha]
2022-05-07 10:02:34 [hahaha]
2022-05-07 10:02:35 [hahaha]
2022-05-07 10:02:36 [hahaha]
2022-05-07 10:02:37 [hahaha]
2022-05-07 10:02:38 [hahaha]
2022-05-07 10:02:39 [hahaha]
2022-05-07 10:02:40 [hahaha]
2022-05-07 10:02:41 [hahaha]
2022-05-07 10:02:42 [context is canceled]

3.2 WithDeadline

这个例子中,生成了一个10秒超时的上下文来调用 Test 函数,Test 函数每一秒都会打印 hahaha,直到 Context 超时,并打印错误信息。

package main

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

func main() {
	ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))
	defer cancel()
	Test(ctx)
}

func Test(ctx context.Context) {
	for range time.Tick(time.Second) {
		select {
		case <-ctx.Done():
			log("context is canceled, err: ", ctx.Err())
			return
		default:
			log("hahaha")
		}
	}
}

func log(msgs ...interface{}) {
	fmt.Println(time.Now().Format("2006-01-02 15:04:05"), msgs)
}

执行结果:

2022-05-07 12:23:32 [hahaha]
2022-05-07 12:23:33 [hahaha]
2022-05-07 12:23:34 [hahaha]
2022-05-07 12:23:35 [hahaha]
2022-05-07 12:23:36 [hahaha]
2022-05-07 12:23:37 [hahaha]
2022-05-07 12:23:38 [hahaha]
2022-05-07 12:23:39 [hahaha]
2022-05-07 12:23:40 [hahaha]
2022-05-07 12:23:41 [hahaha]
2022-05-07 12:23:42 [context is canceled, err:  context deadline exceeded]

3.3 WithTimeout

WithTimeout 本质上就是调用的 WithDeadline,这里的例子和 WithDeadline 基本一致,差别仅在于获取 Context 的地方:

# WithDeadline
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(10*time.Second))
# WithTimeout
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)

3.4 WithValue

这个例子是简单的设置及获取 kv。

package main

import (
	"context"
	"errors"
	"fmt"
)

type testKey string

func main() {
	ctx := context.WithValue(context.Background(), testKey("a"), 1)
	fmt.Println(GetValue(ctx, testKey("a")))
	fmt.Println(GetValue(ctx, testKey("b")))
}

// GetValue 获取Value
func GetValue(ctx context.Context, key testKey) (interface{}, error) {
	v := ctx.Value(key)
	if v == nil {
		return nil, errors.New(fmt.Sprintf("value of %s not found", key))
	}
	return v, nil
}

输出结果为:

1 <nil>
<nil> value of b not found
Go Context Go

Go学习之ArrayGo信号监听