Golang Context 包详解

Posted guangze

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Golang Context 包详解相关的知识,希望对你有一定的参考价值。

Golang Context 包详解

0. 引言

在 Go 语言编写的服务器程序中,服务器通常要为每个 HTTP 请求创建一个 goroutine 以并发地处理业务。同时,这个 goroutine 也可能会创建更多的 goroutine 来访问数据库或者 RPC 服务。
当这个请求超时或者被终止的时候,需要优雅地退出所有衍生的 goroutine,并释放资源。因此,我们需要一种机制来通知衍生 goroutine 请求已被取消。 比如以下例子,sleepRandom_1 的结束就无法通知到 sleepRandom_2

package main

import (
    "fmt"
    "time"
)

func sleepRandom_1() 
    i := 0
    for 
        time.Sleep(1 * time.Second)
        fmt.Printf("This is sleep Random 1: %d\n", i)

        i++
        if i == 5 
            fmt.Println("cancel sleep random 1")
            break
        
    


func sleepRandom_2() 
    i := 0
    for 
        time.Sleep(1 * time.Second)
        fmt.Printf("This is sleep Random 2: %d\n", i)
        i++
    


func main() 

    go sleepRandom_1() // 循环 5 次后退出
    go sleepRandom_2() // 会一直打印 This is sleep Random 2

    for 
        time.Sleep(1 * time.Second)
        fmt.Println("Continue...")
    

1. Context

Context 包提供上下文机制在 goroutine 之间传递 deadline、取消信号(cancellation signals)或者其他请求相关的信息。使用方法是:

  1. 首先,服务器程序为每个接受的请求创建一个 Context 实例(称为根 context,通过 context.Background() 方法创建);
  2. 之后的 goroutine 接受根 context 的一个派生 Context 对象。比如通过调用根 context 的 WithCancel 方法,创建子 context;
  3. goroutine 通过 context.Done() 方法监听取消信号。func Done() <-chan struct 是一个通信操作,会阻塞 goroutine,直到收到取消信号接触阻塞。
    (可以借助 select 语句,如果收到取消信号,就退出 goroutine;否则,默认子句是继续执行 goroutine);
  4. 当一个 Context 被取消(比如执行了 cancelFunc()),那么该 context 派生出来的 context 也会被取消。

1.1 Context 类型

// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface 

    Done() <-chan struct

    Deadline() (deadline time.Time, ok bool)
    
    Err() error
    
    Value(key interface) interface

Done() <-chan struct

Done 方法返回一个 channel,阻塞当前运行的代码,直到以下条件之一发生时,channel 才会被关闭,进而解除阻塞:

  1. WithCancel 创建的 context,cancelFunc 被调用。该 context 以及派生子 context 的 Done channel 都会收到取消信号;
  2. WithDeadline 创建的 context,deadline 到期。
  3. WithTimeout 创建的 context,timeout 到期

Done 要配合 select 语句使用:

// DoSomething 生产数据并发送给通道 out
// 但如果 DoSomething 返回一个则退出函数,
// 或者 ctx.Done 被关闭时也会退出函数.
func Stream(ctx context.Context, out chan<- Value) error 
    for 
        v, err := DoSomething(ctx)
        if err != nil 
            return err
        
        select 
        case <-ctx.Done():
            return ctx.Err()
        case out <- v:
        
    

Deadline() (deadline time.Time, ok bool)

WithDeadline 方法会给 context 设置 deadline,到期自动发送取消信号。调用 Deadline() 返回 deadline 的值。如果没设置,ok 返回 false。
该方法可用于确定当前时间是否临近 deadline。

Err() error

如果 Done 的 channel 被关闭了, Err 函数会返回一个 error,说明错误原因:

  1. 如果 channel 是因为被取消而关闭,打印 canceled;
  2. 如果 channel 是因为 deadline 到时了,打印 deadline exceeded。

重复调用,返回相同值。

Value(key interface) interface

返回由 WithValue 关联到 context 的值。

1.2 创建根 Context

有两种方法创建根 Context:

  1. context.Background()
  2. context.TODO()

根 context 不会被 cancel。这两个方法只能用在最外层代码中,比如 main 函数里。一般使用 Background() 方法创建根 context。
TODO() 用于当前不确定使用何种 context,留待以后调整。

1.3 派生 Context

一个 Context 被 cancel,那么它的派生 context 都会收到取消信号(表现为 context.Done() 返回的 channel 收到值)。
有四种方法派生 context :

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

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

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

  4. func WithValue(parent Context, key, val interface) Context

WithCancel

最常用的派生 context 方法。该方法接受一个父 context。父 context 可以是一个 background context 或其他 context。
返回的 cancelFunc,如果被调用,会导致 Done channel 关闭。因此,绝不要把 cancelFunc 传给其他方法。

WithDeadline

该方法会创建一个带有 deadline 的 context。当 deadline 到期后,该 context 以及该 context 的可能子 context 会受到 cancel 通知。
另外,如果 deadline 前调用 cancelFunc 则会提前发送取消通知。

WithTimeout

与 WithDeadline 类似。创建一个带有超时机制的 context。

WithValue

WithValue 方法创建一个携带信息的 context,可以是 user 信息、认证 token等。该 context 与其派生的子 context 都会携带这些信息。

WithValue 方法的第二个参数是信息的唯一 key。该 key 类型不应对外暴露,为了避免与其他包可能的 key 类型冲突。所以使用 WithValue 也
应像下面例子的方式间接调用 WithValue。

WithValue 方法的第三个参数即是真正要存到 context 中的值。

使用 WithValue 的例子:

package user

import "context"

// User 类型对象会被保存到 Context 中
type User struct 
    // ...


// key 不应该暴露出来。这样避免与包中其他 key 类型冲突
type key int

// userKey 是 user 的 key,不应暴露; 
// 通过 user.NewContext 和 user.FromContext 间接使用 key
var userKey key

// NewContext 返回携带 u 作为 value 的 Context
func NewContext(ctx context.Context, u *User) context.Context 
    return context.WithValue(ctx, userKey, u)


// FromContext 返回关联到 context 的 User类型的 value 的值
func FromContext(ctx context.Context) (*User, bool) 
    u, ok := ctx.Value(userKey).(*User)
    return u, ok

2. 例子

改进引子里的例子。 sleepRandom_1 结束后,会触发 cancelParent() 被调用。所以 sleepRandom_2 中的 ctx.Done() 会被关闭。
sleepRandom_2 执行退出。

package main

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

func sleepRandom_1(stopChan chan struct) 
    i := 0
    for 
        time.Sleep(1 * time.Second)
        fmt.Printf("This is sleep Random 1: %d\n", i)

        i++
        if i == 5 
            fmt.Println("cancel sleep random 1")
            stopChan <- struct
            break
        
    


func sleepRandom_2(ctx context.Context) 
    i := 0
    for 
        time.Sleep(1 * time.Second)
        fmt.Printf("This is sleep Random 2: %d\n", i)
        i++

        select 
        case <-ctx.Done():
            fmt.Printf("Why? %s\n", ctx.Err())
            fmt.Println("cancel sleep random 2")
            return
        default:
        
    


func main() 
    
    ctxParent, cancelParent := context.WithCancel(context.Background())
    ctxChild, _ := context.WithCancel(ctxParent)
    
    stopChan := make(chan struct)

    go sleepRandom_1(stopChan)
    go sleepRandom_2(ctxChild)

    select 
    case <- stopChan:
        fmt.Println("stopChan received")
    
    cancelParent()
    
    for 
        time.Sleep(1 * time.Second)
        fmt.Println("Continue...")
    

3. 参考文档

Go Concurrency Patterns: Context

Understanding the context package in golang

以上是关于Golang Context 包详解的主要内容,如果未能解决你的问题,请参考以下文章

深入理解Golang中的Context包

深入理解Golang中的Context包

golang之context详解

golang 之 context包

golang中context包学习

Golang 之 Context 的迷思