context

本文最后更新于:2023年12月5日 晚上

Context 包定义了上下文类型,该上下文类型跨越 API 边界和进程之间传递截止期限,取消信号和其他请求范围值。

对服务器的传入请求应创建一个 Context,对服务器的传出调用应接受 Context。它们之间的函数调用链必须传播 Context,可以用使用 WithCancel,WithDeadline,WithTimeout 或 WithValue 创建的派生上下文替换。当 Context 被取消时,从它派生的所有 Context 也被取消。

WithCancel,WithDeadline 和 WithTimeout 函数采用 Context(父级)并返回派生的 Context(子级)和 CancelFunc。调用 CancelFunc 将取消子对象及其子对象,删除父对子对象的引用,并停止任何关联的定时器。未能调用 CancelFunc 会泄漏子项及其子项,直至父项被取消或计时器激发。go vet 工具检查在所有控制流路径上使用 CancelFuncs。

使用 Contexts 的程序应该遵循这些规则来保持包之间的接口一致,并使静态分析工具能够检查上下文传播:

不要将上下文存储在结构类型中;相反,将一个 Context 明确地传递给每个需要它的函数。上下文应该是第一个参数,通常命名为 ctx:

func DoSomething(ctx context.Context, arg Arg) error {
 // ... use ctx ...
}

即使函数允许,也不要传递 nil Context。如果您不确定要使用哪个 Context,请传递 context.TODO。

使用上下文值仅适用于传输进程和 API 的请求范围数据,而不用于将可选参数传递给函数。

相同的上下文可以传递给在不同 goroutine 中运行的函数; 上下文对于多个 goroutine 同时使用是安全的。

有关使用 Contexts 的服务器的示例代码,请参阅https://blog.golang.org/context。

Variables

Canceled

var Canceled = errors.New("context canceled")

Canceled is the error returned by Context.Err when the context is canceled.

DeadlineExceeded

var DeadlineExceeded error = deadlineExceededError{}

DeadlineExceeded is the error returned by Context.Err when the context’s deadline passes. 截止时间过后返回的错误

type CancelFunc

type CancelFunc func()

func WithCancel

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

WithCancel函数用来创建一个可取消的context,返回一个context和一个CancelFunc,调用CancelFunc即可触发cancel操作。

取消 Context 会释放与它相关的资源,所以只要完成在 Context 中运行的操作,代码就应该调用 cancel。

示例:

这个例子演示了如何使用可取消的 context 来防止 goroutine 泄漏。在示例函数结束时,由 gen 启动的 goroutine 将返回而不会泄漏。

package main

import (
 "context"
 "fmt"
)

func main() {
 gen := func(ctx context.Context) <-chan int {
  dst := make(chan int)
  n := 1
  go func() {
   for {
    select {
    case <-ctx.Done():
     return // returning not to leak the goroutine 返回不泄漏goroutine
    case dst <- n:
     n++
    }
   }
  }()
  return dst
 }

 ctx, cancel := context.WithCancel(context.Background())
 defer cancel() // cancel when we are finished consuming integers

 for n := range gen(ctx) {
  fmt.Println(n)
  if n == 5 {
   break
  }
 }
}

// output:
// 1
// 2
// 3
// 4
// 5

func WithDeadline

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

WithDeadline返回一个基于parent的可取消的context,并且其过期时间deadline不晚于所设置时间d

如果父节点parent有过期时间并且过期时间早于给定时间d,那么新建的子节点context无需设置过期时间,使用WithCancel创建一个可取消的context即可;

示例:

d := time.Now().Add(50 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), d)

// Even though ctx will be expired, it is good practice to call its
// cancelation function in any case. Failure to do so may keep the
// context and its parent alive longer than necessary.
defer cancel()

select {
case <-time.After(1 * time.Second):
    fmt.Println("overslept")
case <-ctx.Done():
    fmt.Println(ctx.Err())
}

func WithTimeout

WithDeadline类似,WithTimeout也是创建一个定时取消的context,只不过WithDeadline是接收一个过期时间点,而WithTimeout接收一个相对当前时间的过期时长timeout:

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
    return WithDeadline(parent, time.Now().Add(timeout))
}

type Context

context 包含截止日期取消信号以及跨越 API 边界的其他值

上下文的方法可能会被多个 goroutine 同时调用。

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key interface{}) interface{}
}
  • Deadline返回绑定当前context的任务被取消的截止时间;如果没有设定期限,将返回ok == false
  • Done 当绑定当前context的任务被取消时,将返回一个关闭的channel;如果当前context不会被取消,将返回nil
  • Err 如果Done返回的channel没有关闭,将返回nil;如果Done返回的channel已经关闭,将返回非空的值表示任务结束的原因。如果是context被取消,Err将返回Canceled;如果是context超时,Err将返回DeadlineExceeded
  • Value 返回context存储的键值对中当前key对应的值,如果没有对应的key,则返回nil

func Background

emptyCtx是一个int类型的变量,但实现了context的接口。emptyCtx没有超时时间,不能取消,也不能存储任何额外信息,所以emptyCtx用来作为context树的根节点。

// 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 interface{}) interface{} {
    return nil
}

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

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

func Background() Context {
    return background
}

func TODO() Context {
    return todo
}

但我们一般不会直接使用emptyCtx,而是使用由emptyCtx实例化的两个变量,分别可以通过调用BackgroundTODO方法得到,这两个context在实现上是一样的。

func TODO

BackgroundTODO 方法在某种层面上看其实也只是互为别名,两者没有太大的差别,不过 context.Background() 是上下文中最顶层的默认值,所有其他的上下文都应该从 context.Background() 演化出来。

1646128337346

我们应该只在不确定时使用 context.TODO(),在多数情况下如果函数没有上下文作为入参,我们往往都会使用 context.Background() 作为起始的 Context 向下传递。

func WithValue

WithValue用以向context添加键值对:

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

这里添加键值对不是在原context结构体上直接添加,而是以此context作为父节点,重新创建一个新的valueCtx子节点,将键值对添加在子节点上,由此形成一条context链。获取value的过程就是在这条context链上由尾部上前搜寻。

返回parent的一个副本,调用该副本的 Value(key)方法将得到 val。这样我们不光将根节点原有的值保留了,还在子孙节点中加入了新的值,注意若存在 Key 相同,则会被覆盖。

注意:为了避免使用 context 的包之间发生冲突,key 必须具有可比性( comparable ),不应该是 string 或者其他任何内置类型。应该使用自定义类型。

总结

Go 语言中的 Context 的主要作用还是在多个 Goroutine 或者模块之间同步取消信号或者截止日期,用于减少对资源的消耗和长时间占用,避免资源浪费,虽然传值也是它的功能之一,但是这个功能我们还是很少用到。

在真正使用传值的功能时我们也应该非常谨慎,不能将请求的所有参数都使用 Context 进行传递,这是一种非常差的设计,比较常见的使用场景是传递请求对应用户的认证令牌以及用于进行分布式追踪的请求 ID。

context包通过构建树型关系的 Context,来达到上一层 Goroutine 能对传递给下一层 Goroutine 的控制。对于处理一个 Request 请求操作,需要采用context来层层控制 Goroutine,以及传递一些变量来共享。

  • Context 对象的生存周期一般仅为一个请求的处理周期。即针对一个请求创建一个 Context 变量(它为 Context 树结构的根);在请求处理结束后,撤销此 ctx 变量,释放资源。
  • 每次创建一个 Goroutine,要么将原有的 Context 传递给 Goroutine,要么创建一个子 Context 并传递给 Goroutine。
  • Context 能灵活地存储不同类型、不同数目的值,并且使多个 Goroutine 安全地读写其中的值。
  • 当通过父 Context 对象创建子 Context 对象时,可同时获得子 Context 的一个撤销函数,这样父 Context 对象的创建环境就获得了对子 Context 将要被传递到的 Goroutine 的撤销权。

使用原则

  • 不要把 Context 存在一个结构体当中,显式地传入函数。Context 变量需要作为第一个参数使用,一般命名为 ctx;
  • 即使方法允许,也不要传入一个 nil 的 Context,如果你不确定你要用什么 Context 的时候传一个 context.TODO;
  • 使用 context 的 Value 相关方法只应该用于在程序和接口中传递的和请求相关的元数据,不要用它来传递一些可选的参数;
  • 同样的 Context 可以用来传递到不同的 goroutine 中,Context 在多个 goroutine 中是安全的;
  • 在子 Context 被传递到的 goroutine 中,应该对该子 Context 的 Done 信道(channel)进行监控,一旦该信道被关闭(即上层运行环境撤销了本 goroutine 的执行),应主动终止对当前请求信息的处理,释放资源并返回。

通常,context.Background()作为最顶层的 Context,在此基础上创建可撤销的 Context。


context
http://blog.lujinkai.cn/Golang/标准库/context/
作者
像方便面一样的男子
发布于
2022年9月24日
更新于
2023年12月5日
许可协议