Go的context库(包)

  • Post author:
  • Post category:其他




一、问题的提出:

在 Go http包的Server中,

也就是在go的http服务器中,对每一个请求request都会有一个对应的 goroutine 去处理该请求,而且对应的请求处理函数在处理该请求时有可能又会启动新的的 goroutine 用来访问一些后端服务:比如数据库和RPC服务。

而且用来处理同一个请求request的 各goroutine间 通常都需要访问与该request相关的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间,那么如何方便的在这些goroutine间(也就是在与处理该request相关的各goroutine间)传递该request域的数据,以及 如何在该request被取消或超时时,及时的关闭所有用来处理该请求的 goroutine?原则上当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。



二、解决方案:


Go 的Context库就是用来解决以上问题的。Go1.7加入了一个新的标准库context,它定义了Context类型。


1、通过

func WithValue(parent Context, key, val interface{}) Context

实现request域数据与Context对象的绑定,并且可以通过在不同的goroutine间传递该Context对象或者其派生Context对象来解决:在这些goroutine间(也就是在与处理该request相关的各goroutine间)传递该request域的数据的问题,且各goroutine队该context对象的值操作是并发安全的!!!



2、可以通过往子goroutine或者处理同一个request请求的各goroutine传递一个Context对象,然后通过该传递来的Context的Done通道中是否有值来判定当前任务是否被Cancel或者执行超时了 。这个Done通道即为Done()方法返回的channel通道,当context因超时或者到了截止时间或者被手动cancel时,会自动往其Done通道中存一个值,反言之只要能从Context的Done通道中取出一个值就表示该Context被cancel或者中止了,也就是表示该goroutine的任务被取消了其应被及时关闭,释放其所占的资源。



三、 Context接口


context.Context是一个接口,该接口定义了四个需要实现的方法

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

其中:


  • Deadline()方法需要返回当前Context被取消的时间,也就是完成工作的截止时间(deadline);


  • Done()方法需要返回一个只读Channel,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done()方法也只会返回同一个Channel;


  • Err()方法会返回当前Context结束的原因,它只会在Done返回的Channel被关闭时才会返回非空的值也就是返回一个err告知当前Context结束的原因:

    1)如果当前Context被取消就会返回Canceled错误;

    2)如果当前Context超时就会返回DeadlineExceeded错误;


  • Value()方法会从Context中返回键对应的值,对于同一个上下文来说,多次调用Value并传入相同的Key会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;



【注意】:只要父Context对象关闭了,那么其所有的子(派生)Context对象也会被关闭!!!



四、生成顶层(根)Context对象:Background()和TODO()


context库内置了两个函数:Background()和TODO(),这两个函数均是返回一个空的Context对象(分别对应变量background以及todo);我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context,衍生出更多的子上下文对象。



它们的使用区别如下:


  • Background()主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。

  • TODO():如果我们不知道该使用什么Context的时候,可以使用这个。


background和todo本质上都是一个实现了Context接口的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"
}



五、创建当前(父)Context的派生(子)Context对象的四个WithXXX()方法


1、WithCancel()


  • 创建一个可手动cancel的parent Context对象的派生context对象(或者叫parent Context的副本)以及一个用于手动cancel该派生context的回调函数cancelFunc()

  • 当我们手动调用返回的cancel函数或者当关闭父Context(的Done通道)时,将关闭返回的派生Context 对象(的Done通道,这个Done通道即为Done()返回的channel通道),同时往该 Done通道内存入一个值。此时如果能从该Context的Done通道内取出一值即为表示任务被cancel或者执行超时了,应立即释放与该context有关的资源,并关闭接受该context的goroutine(因为其运行的任务被cancel或者因为执行超时被迫关闭),释放其所占的资源


方法签名:

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


示例:

func gen(ctx context.Context) <-chan int {
        dst := make(chan int)
        n := 1
        go func() {
            for {
                select {
                case <-ctx.Done():
                    return // return结束该goroutine,防止泄露
                case dst <- n:
                    n++
                }
            }
        }()
        return dst
    }
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // 当我们取完需要的整数后调用cancel

    for n := range gen(ctx) {
        fmt.Println(n)
        if n == 5 {
            break
        }
    }
}
//上面的示例代码中,gen函数在单独的goroutine中生成整数并将它们发送到返回的通道。 gen的调用者在使用生成的整数之后需要取消上下文,以免gen启动的内部goroutine发生泄漏。


2、WithDeadline()


  • 创建一个可以指定截止时间deadline的该传入context对象的派生context对象,以及一个用于手动cancel该派生context的回调函数cancelFunc()

  • 当到截止日过期deadline时,或者当调用返回的cancel函数时,或者当parent Context(的Done通道)关闭时,返回的派生Context对象(的Done通道)将被关闭,同时往该 Done通道内存入一个值。此时如果能从该Context的Done通道内取出一值即为表示任务被cancel或者执行超时了,应立即释放与该context有关的资源,并关闭该goroutine的运行,释放其所占的资源


方法签名

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


示例:

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

    // 尽管ctx会过期,但在任何情况下调用它的cancel函数都是很好的实践。
    // 如果不这样做,可能会使上下文及其父类存活的时间超过必要的时间。
    defer cancel()

    select {
    case <-time.After(1 * time.Second):
        fmt.Println("overslept")
    case <-ctx.Done():
        fmt.Println(ctx.Err())
    }
}
上面的代码中,定义了一个50毫秒之后过期的deadline,然后我们调用context.WithDeadline(context.Background(), d)得到一个上下文(ctx)和一个取消函数(cancel),然后使用一个select让主程序陷入等待:等待1秒后打印overslept退出或者等待ctx过期后退出。 因为ctx50秒后就过期,所以ctx.Done()会先接收到值,上面的代码会打印ctx.Err()取消原因。


3、 WithTimeout


  • 创建一个可以指定超时时间timeout的该传入context对象的派生context对象(底层调用的还是WithDeadline(),也就是相当于调用了

    WithDeadline(parent, time.Now().Add(timeout))

    ),以及一个用于手动cancel该派生context的回调函数cancelFunc()

  • 当过了超时时间timeout时,或者当调用返回的cancel函数时,或者当parent Context(的Done通道)关闭时,返回的派生Context对象(的Done通道)将被关闭,同时往该 Done通道内存入一个值。此时如果能从该Context的Done通道内取出一值即为表示任务被cancel或者执行超时了,应立即释放与该context有关的资源,并关闭该goroutine的运行,释放其所占的资源


函数签名:

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

通常用于数据库或者网络连接的超时控制。具体示例如下:

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

// context.WithTimeout

var wg sync.WaitGroup

func worker(ctx context.Context) {
LOOP:
    for {
        fmt.Println("db connecting ...")
        time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
        select {
        case <-ctx.Done(): // 50毫秒后自动调用
            break LOOP
        default:
        }
    }
    fmt.Println("worker done!")
    wg.Done()
}

func main() {
    // 设置一个50毫秒的超时
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 5)
    cancel() // 通知子goroutine结束
    wg.Wait()
    fmt.Println("over")
}


4、 WithValue()


  • WithValue()函数能够将请求作用域的数据与 Context 对象建立关系:创建一个持有key-val 这个键值对的传入的 parent context对象的派生context对象


  • 只应在API和进程间传递请求域的数据时使用Context值,而不是使用它来传递可选参数给函数。

  • 所提供的键必须是可比较的,并且不应该是string类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。WithValue的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}。或者,导出的上下文关键变量的静态类型应该是指针或接口。


函数声明:

    func WithValue(parent Context, key, val interface{}) Context


示例:

package main

import (
    "context"
    "fmt"
    "sync"

    "time"
)

// context.WithValue

type TraceCode string

var wg sync.WaitGroup

func worker(ctx context.Context) {
    key := TraceCode("TRACE_CODE")
    traceCode, ok := ctx.Value(key).(string) // 在子goroutine中获取trace code
    if !ok {
        fmt.Println("invalid trace code")
    }
LOOP:
    for {
        fmt.Printf("worker, trace code:%s\n", traceCode)
        time.Sleep(time.Millisecond * 10) // 假设正常连接数据库耗时10毫秒
        select {
        case <-ctx.Done(): // 50毫秒后自动调用
            break LOOP
        default:
        }
    }
    fmt.Println("worker done!")
    wg.Done()
}

func main() {
    // 设置一个50毫秒的超时
    ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50)
    // 在系统的入口中设置trace code传递给后续启动的goroutine实现日志数据聚合
    ctx = context.WithValue(ctx, TraceCode("TRACE_CODE"), "12512312234")
    wg.Add(1)
    go worker(ctx)
    time.Sleep(time.Second * 5)
    cancel() // 通知子goroutine结束
    wg.Wait()
    fmt.Println("over")
}



版权声明:本文为weixin_44718747原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。