Go Context · 2023年12月9日 0

理解 Go 语言中的 Context【】

如果你是 Golang 新手,你有可能几乎在任何地方都见过这个模块,但有时会发现它令人困惑,甚至是抽象的。

事实是,这个模块无处不在是有原因的。它在维护你的应用程序性能方面起着至关重要的作用。

在这篇博文中,我将为你省去翻阅源代码的麻烦,并告诉你关于该模块的一切!context

让我们开始吧!

一、为什么我们需要 Context ?

想象一下,你是一家餐厅的接单员。

当一个订单到达时,你把它委托给众多厨师中的一个。

如果顾客突然决定走开,你会怎么做?

毫无疑问,你会阻止你的厨师进一步处理这个订单,以防止任何原料的浪费!

这就是这个 context 的作用!

它是传递给你的函数和 Goroutines 的一个参数,并允许你在不再需要它们时及时停止它们。

二、什么是 Context?

该模块的典型用法是当客户端终止与服务器的连接时。

如果终止发生在服务器正在进行一些繁重的工作或数据库查询时呢?

该模块允许这些处理在不再需要的情况下立即停止。

Context 使用可归结为三个主要部分:

  • 侦听取消事件
  • 发出一个取消事件
  • 传递请求范围数据

让我们分别讨论这些内容。

三、监听取消事件

image.png

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

Context 类型只不过是一个接口,实现了四个简单的函数。

现在,我们先关注前两个:Done() 和 Err()

当 context 被 cancel 时,Done() 函数返回一个接收空 struct 的 channel。

Err() 函数在 cancel 的情况下返回一个非 nil 的 error,否则返回 nil。

使用这些函数,监听取消事件变得很容易。

1. 监听 Done() 通道

go

复制代码
func main() {
    http.ListenAndServe(":8000", http.HandlerFunc(handler))
}

func handler(
  w http.ResponseWriter, 
  r *http.Request,
) {
    ctx := r.Context()
    
    select {
        case <-time.After(2 * time.Second):
            w.Write([]byte("request processed"))
    
        case <- ctx.Done(): 
            fmt.Println("request cancelled")
            return
    }
}

在上面的例子中,我们模拟了一个网络服务处理器。

我们用 time.After() 来模拟了一个需要两秒钟处理一个请求的函数。

如果 context 在两秒内被取消,ctx.Done() 会收到一个空的 struct 。第二种情况将被执行,并且退出函数。

你可以在你的本地启动这段代码。一旦启动,在你的浏览器上访问 localhost:8000,然后在两秒内关闭它。观察你的终端,看看会发生什么。

2. 检查 Err() 的错误

 

复制代码
func handler(ctx context.Context) {
    if ctx.Err() != nil {
        fmt.Println("Context is cancelled")
        return
    }
    
    fmt.Println("Processing request")
}

另外,你可以在执行一些关键逻辑之前从 ctx.Err() 检查错误。

如果上下文被取消了,上面的函数就会停止并返回。

四、发出一个取消事件

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

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

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

context 提供了三个返回 CancelFunc 的函数。

调用 cancelFunc 会向 ctx.Done() 通道发射一个空的 struct,并通知监听它的下游函数。

在深入研究它们之前,我们先来谈谈上下文树(context tree)和根上下文(root context)。

1. Context Tree

当你调用这些 WithX 函数时,它们接受一个父级 context,并返回一个带有新的 Done 通道的 context 副本。

less

复制代码
rootCtx := context.Background()

child1Ctx, cancelFunc1 := context.WithCancel(rootCtx)
child2Ctx, cancelFunc2 := context.WithCancel(rootCtx)

child3Ctx, cancelFunc3 := context.WithCancel(child1Ctx)

在上面的例子中,我们创建了一个多上下文树(context tree)。

当我们调用 cancelFunc1,我们将取消 child1Ctx 和 child3Ctx,child2Ctx不受影响。

2. Root context

由于这些函数需要一个父 context 作为参数,context 提供了两个简单的函数来创建 root context。

 

复制代码
func Background() Context

func TODO() Context

这些函数输出一个空的 context,完全不做任何事情。它不能被取消,也不能携带值。

它们的主要目的是作为一个根 context,以后可以传递给任何一个 WithX 函数来创建一个可取消的 context。

3. WithCancel

go

复制代码
ctx := context.Background()

ctx, cancel := context.WithCancel(ctx)
cancel()

// Output: context cancelled
fmt.Println(ctx.Err().Error())

WithCancel 函数接收一个父 context,并返回一个可取消的 contet 和一个取消函数。

go

复制代码
func handler() {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    
    go operation1(ctx)
  
    data, err := databaseQuery()
    if err != nil {
        cancel()
    }
}

func operation1(ctx context.Context) {
    for {
        select {
            case <- ctx.Done():
                fmt.Println(ctx.Err().Error())
                return

            default:
                fmt.Println("Do something")
        }
    }
}

如果 databaseQuery 返回一个错误,cancel() 将被调用。 然后 operation1 函数将通过 ctx.Done() 被通知并优雅地退出。

你可以通过 ctx.Err() 找到取消的原因。

4. WithTimeout

WithTimeout 允许你指定一个超时时间,如果超过这个时间,就自动取消上下文。

go

复制代码
func handler() {
    ctx := context.Background()

    ctx, cancel := context.WithTimeout(
        ctx, 
        3*time.Second,
    )
    defer cancel()

    dataChan := make(chan string)
    go databaseQuery(dataChan)

    select {
        case <- dataChan:
            fmt.Println("Query succeeds, do something")
        
        case <- ctx.Done():
            fmt.Println("Timeout exceeded, returning")
            return
    }
}

在上面的例子中,上下文将在三秒后自动取消。

因此,如果在这之前数据库查询没有成功,处理程序将退出并返回。

或者,你也可以通过 cancel() 函数手动取消上下文。

5. WithDeadline

WithDeadline 函数接受一个特定的超时时间,而不是一个持续时间。除此之外,它的工作原理与 WithTimeout 完全相似。

go

复制代码
func handler() {
    ctx := context.Background()
  
    ctx, cancel := context.WithDeadline(
        ctx, 
        time.Now().Add(time.Second*3),
    )
  
    defer cancel()
}

上面例子中的上下文将在三秒后自动取消。

五、传递请求范围数据

正如我们通常在函数之间传递 ctx 变量一样,请求范围数据可以使用 WithValue 函数标记该变量。

考虑到一个涉及多个函数调用的应用程序,我们可以通过 ctx 变量将 traceID 传递给这些函数以进行监控和打印日志。

go

复制代码
const traceIDKey = "trace_id"

func main() {
    ctx := context.Background()
    ctx = context.WithValue(
        ctx, 
        traceIDKey, 
        "random_id123",
    )
  
    function1(ctx)
}

func function1(ctx context.Context) {
    log.Println(
        "Entered function 1 with traceID: ",
        ctx.Value(traceIDKey),
    )
  
    function2(ctx) 
}

func function2(ctx context.Context) {
    log.Println(
        "Entered function 2 with traceID: ",
        ctx.Value(traceIDKey),
    )
}

WithValue 函数可以将值添加到 ctx 变量中,然后通过 ctx.Value 函数可以读取出来。

六、注意事项和做法

虽然很方便,但 context 经常被误用,并且很容易给你的应用程序带来错误。

在我们结束这篇文章之前,让我们谈谈一些基本的做法。

1. 总是在 defer 中调用 cancel()

css

复制代码
ctx := context.Background()

ctx, cancel := context.WithCancel(ctx)

defer cancel()

当你通过 WithCancel 函数生成一个新的可取消的 context 时,该模块将:

  • 如果调用了 cancel(),将取消事件传播给所有的子 Goroutine
  • 跟踪父 context 结构中的所有子 context

如果一个函数在没有调用 cancel() 的情况下返回,Goroutine和 子的 context 将无限期地留在内存中,导致内存泄漏。

这也适用于 WithTimeout 和 WithDeadline,除了这些函数在超过截止日期时自动取消上下文。

但是,对于任何一个函数来说,在 defer 中取消上下文仍然是一个最佳做法。

2. 只对请求范围数据使用 WithValue

go

复制代码
ctx := context.Background()

ctx1 := context.WithValue(ctx, "key1", "value1")

ctx1 := context.WithValue(ctx1, "key1", "value2")

我们很容易认为,我们是在最后一次函数调用中用 value2 覆盖了 value1。

然而,情况并非如此。

WithValue 函数接收了一个父级上下文并返回了一个副本。因此,它不是覆盖值,而是用一个新的键值对创建一个新的副本。

因此,你应该将 WithValue 的使用限制为有限的请求范围数据。

传递稍后将被改变的参数值将导致创建多个 context 变量,这会导致你的内存使用量大幅增加。

七、总结

关于 context 的讲解就到此为止!

go

复制代码
type Context interface {
    // Channel listen to cancellation
    Done() <-chan struct{}

    // Return error if context is cancelled
    Err() error
    
    // Return the deadline set to the context
    Deadline() (deadline time.Time, ok bool)
  
    // Return the value for a given key
    Value(key interface{}) interface{}
}

// Return empty root context
func Background() Context
func TODO() Context

// Return cancellable context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

// Return context with key-value pairs
func WithValue(parent Context, key, val interface{}) Context

上面的要点总结了 context 所提供的所有内容。

希望这篇文章对你有帮助,我们下次再会。

medium.com/rungo/under…

作者:非著名开发者
链接:https://juejin.cn/post/7130293077893185544
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
打赏 赞(0) 分享'
分享到...
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

文章目录