写点什么

Go 语言 context 都能做什么?

作者:AlwaysBeta
  • 2023-07-02
    北京
  • 本文字数:3513 字

    阅读完需:约 12 分钟

原文链接: Go 语言 context 都能做什么?


很多 Go 项目的源码,在读的过程中会发现一个很常见的参数 ctx,而且基本都是作为函数的第一个参数。


为什么要这么写呢?这个参数到底有什么用呢?带着这样的疑问,我研究了这个参数背后的故事。


开局一张图:



核心是 Context 接口:


// A Context carries a deadline, cancelation signal, and request-scoped values// across API boundaries. Its methods are safe for simultaneous use by multiple// goroutines.type Context interface {    // Done returns a channel that is closed when this Context is canceled    // or times out.    Done() <-chan struct{}
// Err indicates why this context was canceled, after the Done channel // is closed. Err() error
// Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none. Value(key interface{}) interface{}}
复制代码


包含四个方法:


  • Done():返回一个 channel,当 times out 或者调用 cancel 方法时。

  • Err():返回一个错误,表示取消 ctx 的原因。

  • Deadline():返回截止时间和一个 bool 值。

  • Value():返回 key 对应的值。


有四个结构体实现了这个接口,分别是:emptyCtx, cancelCtx, timerCtxvalueCtx


其中 emptyCtx 是空类型,暴露了两个方法:


func Background() Contextfunc TODO() Context
复制代码


一般情况下,会使用 Background() 作为根 ctx,然后在其基础上再派生出子 ctx。要是不确定使用哪个 ctx,就使用 TODO()


另外三个也分别暴露了对应的方法:


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
复制代码

遵循规则

在使用 Context 时,要遵循以下四点规则:


  1. 不要将 Context 放入结构体,而是应该作为第一个参数传入,命名为 ctx

  2. 即使函数允许,也不要传入 nil 的 Context。如果不知道用哪种 Context,可以使用 context.TODO()

  3. 使用 Context 的 Value 相关方法只应该用于在程序和接口中传递和请求相关的元数据,不要用它来传递一些可选的参数。

  4. 相同的 Context 可以传递给不同的 goroutine;Context 是并发安全的。

WithCancel

func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
复制代码


WithCancel 返回带有新 Done 通道的父级副本。当调用返回的 cancel 函数或关闭父上下文的 Done 通道时,返回的 ctxDone 通道将关闭。


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


举个例子:


这段代码演示了如何使用可取消上下文来防止 goroutine 泄漏。在函数结束时,由 gen 启动的 goroutine 将返回而不会泄漏。


package main
import ( "context" "fmt")
func main() { // gen generates integers in a separate goroutine and // sends them to the returned channel. // The callers of gen need to cancel the context once // they are done consuming generated integers not to leak // the internal goroutine started by gen. 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 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 } }}
复制代码


输出:


12345
复制代码

WithDeadline

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
复制代码


WithDeadline 返回父上下文的副本,并将截止日期调整为不晚于 d。如果父级的截止日期已经早于 d,则 WithDeadline(parent, d) 在语义上等同于 parent


当截止时间到期、调用返回的取消函数时或当父上下文的 Done 通道关闭时,返回的上下文的 Done 通道将关闭。


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


举个例子:


这段代码传递具有截止时间的上下文,来告诉阻塞函数,它应该在到达截止时间时立刻退出。


package main
import ( "context" "fmt" "time")
const shortDuration = 1 * time.Millisecond
func main() { d := time.Now().Add(shortDuration) ctx, cancel := context.WithDeadline(context.Background(), d)
// Even though ctx will be expired, it is good practice to call its // cancellation 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()) }}
复制代码


输出:


context deadline exceeded
复制代码

WithTimeout

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
复制代码


WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))


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


举个例子:


这段代码传递带有超时的上下文,以告诉阻塞函数应在超时后退出。


package main
import ( "context" "fmt" "time")
const shortDuration = 1 * time.Millisecond
func main() { // Pass a context with a timeout to tell a blocking function that it // should abandon its work after the timeout elapses. ctx, cancel := context.WithTimeout(context.Background(), shortDuration) defer cancel()
select { case <-time.After(1 * time.Second): fmt.Println("overslept") case <-ctx.Done(): fmt.Println(ctx.Err()) // prints "context deadline exceeded" }
}
复制代码


输出:


context deadline exceeded
复制代码

WithValue

func WithValue(parent Context, key, val any) Context
复制代码


WithValue 返回父级的副本,其中与 key 关联的值为 val


其中键必须是可比较的,并且不应是字符串类型或任何其他内置类型,以避免使用上下文的包之间发生冲突。 WithValue 的用户应该定义自己的键类型。


为了避免分配给 interface{},上下文键通常具有具体的 struct{} 类型。或者,导出的上下文键变量的静态类型应该是指针或接口。


举个例子:


这段代码演示了如何将值传递到上下文以及如何检索它(如果存在)。


package main
import ( "context" "fmt")
func main() { type favContextKey string
f := func(ctx context.Context, k favContextKey) { if v := ctx.Value(k); v != nil { fmt.Println("found value:", v) return } fmt.Println("key not found:", k) }
k := favContextKey("language") ctx := context.WithValue(context.Background(), k, "Go")
f(ctx, k) f(ctx, favContextKey("color"))}
复制代码


输出:


found value: Gokey not found: color
复制代码


本文的大部分内容,包括代码示例都是翻译自官方文档,代码都是经过验证可以执行的。如果有不是特别清晰的地方,可以直接去读官方文档。


以上就是本文的全部内容,如果觉得还不错的话欢迎点赞转发关注,感谢支持。




官方文档:


  • https://pkg.go.dev/context@go1.20.5


源码分析:


  • https://mritd.com/2021/06/27/golang-context-source-code/

  • https://www.qtmuniao.com/2020/07/12/go-context/

  • https://seekload.net/2021/11/28/go-context.html


推荐阅读:


发布于: 刚刚阅读数: 3
用户头像

AlwaysBeta

关注

微信公众号:AlwaysBeta 2017-11-30 加入

专注分享后端开发技术干货!

评论

发布
暂无评论
Go 语言 context 都能做什么?_Go_AlwaysBeta_InfoQ写作社区