context 包实践与理解

本贴最后更新于 2000 天前,其中的信息可能已经时异事殊

context 包是干嘛的? 这是我刚开始了解时最大的疑惑。
简单来说就是上下文,用来追踪信息的一棵树,我们可以对这棵树的节点进行删除和增加,在这个节点上保存数据

应用场景:

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

context 原理
context 的调用时链式的通过 context 包中的几个函数来进行派生或取消 WithCancel WithDeadline WithTimeout WithValue
父 contenxt(父节点)取消时其派生的子节点都将取消

通过 context.WithXXX 都将返回新的 ContextCancelFunc(这是取消的一个回调)。调用 CancelFunc 将取消子代,移除父代对子代的引用,并且停止所有定时器。未能调用 CancelFunc 将泄漏子代,直到父代被取消或定时器触发。go vet 工具检查所有流程控制路径上使用 CancelFuncs

context 包接口如下

type Context interface {              
    // 当Context 被 canceled 或是 times out 的时候,Done 返回一个被 closed 的channel     
    Done() <-chan struct{}       
 
    // 在 Done 的 channel被closed 后, Err 代表被关闭的原因  
    Err() error
 
    // 如果存在,Deadline 返回Context将要关闭的时间 
    Deadline() (deadline time.Time, ok bool)
 
    // 如果存在,Value 返回与 key 相关了的值,不存在返回 nil 
    Value(key interface{}) interface{}
}

所有方法

func Background() Context
func TODO() Context

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 是一个接口,想要使用就得实现其方法。在 context 包内部已经为我们实现好了两个空的 Context,可以通过调用 Background()TODO() 方法获取。一般的将它们作为 Context 的根,往下派生

WithCancel
     func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
         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,
             done:    make(chan struct{}),
         }
     }

以一个新的 Done channel 返回一个父 Context 的拷贝

此示例(来自官方包 demo)演示使用一个可取消的上下文,以防止 goroutine 泄漏。示例函数结束时,defer 调用 cancel 方法,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
        }
    }
}

WithDeadLine

简单来说就是在在设定的时间内执行通道关闭操作。

情况 1、上下文父节点关闭早于当前节点关闭,此时当前节点也关闭。

情况 2、在指定时间进行关闭函数操作。

func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) {
         if cur, ok := parent.Deadline(); ok && cur.Before(deadline) {
             // The current deadline is already sooner than the new one.
             return WithCancel(parent)
         }
         c := &timerCtx{
             cancelCtx: newCancelCtx(parent),
             deadline:  deadline,
         }

官方 demo:

package main

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

func main() {
    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())
    }
}


WithTimeout

顾名思义是用来是超时处理的,WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))

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

官方 demo:

package main

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

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(), 50*time.Millisecond)
    defer cancel()

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


WithValue
 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}
     }

简单来说就是用来存 key-value 的 map, 并且当前节点及其后代的节点均可以拿到此 map 中的值

WithValue 返回的父与键关联的值在 val 的副本。

使用上下文值仅为过渡进程和 Api 的请求范围的数据,而不是将可选参数传递给函数。

提供的键必须是可比性和应该不是字符串类型或任何其他内置的类型以避免包使用的上下文之间的碰撞。WithValue 用户应该定义自己的键的类型。为了避免分配分配给接口 {} 时,上下文键经常有具体类型结构 {}。另外,导出的上下文关键变量静态类型应该是一个指针或接口。

官方 demo:

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"))
}


参考 Deepzz 的博客

参考 go 语言中文社区

  • golang

    Go 语言是 Google 推出的一种全新的编程语言,可以在不损失应用程序性能的情况下降低代码的复杂性。谷歌首席软件工程师罗布派克(Rob Pike)说:我们之所以开发 Go,是因为过去 10 多年间软件开发的难度令人沮丧。Go 是谷歌 2009 发布的第二款编程语言。

    492 引用 • 1383 回帖 • 375 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...