go语言context包

发布时间 2023-08-18 17:04:14作者: 自然洒脱

context包是在go1.7版本中引入到标准库中的

context可以用来在goroutine之间传递上下文信息,相同的context可以传递给运行在不同goroutine中的函数,上下文对于多个goroutine同时使用是安全的

context被当作第一个参数(官方建议),并且不断透传下去,基本一个项目代码中到处都是context

context的作用就是在不同的goroutine之间同步请求特定的数据、取消信号以及处理请求的截止日期。

https://pkg.go.dev/context@go1.20#Context

Context其实就是一个接口,定义了四个方法:

type Context interface {
        // 当Context自动取消或者到了取消时间被取消后返回
    Deadline() (deadline time.Time, ok bool)
        // 当Context被取消或者到了deadline返回一个被关闭的channel
    Done() <-chan struct{}
        // 当Context被取消或者关闭后,返回context取消的原因
    Err() error
        // 获取设置的key对应的值
    Value(key any) any
}

创建和衍生context

context包主要提供了两种方式创建context:

  • context.Backgroud()
  • context.TODO()

这两个函数其实只是互为别名,没有差别,官方给的定义是:

  • context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。
  • context.TODO 应该只在不确定应该使用哪种上下文时使用;

所以在大多数情况下,我们都使用context.Background作为起始的上下文向下传递。

上面的两种方式是创建根context,不具备任何功能,具体实践还是要依靠context包提供的With系列函数来进行派生:

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树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个

 withValue携带数据

在有trace id需求时,可使用此方法,在使用withVaule时要注意四个事项:

  • 不建议使用context值传递关键参数,关键参数应该显示的声明出来,不应该隐式处理,context中最好是携带签名、trace_id这类值。
  • 因为携带value也是keyvalue的形式,为了避免context因多个包同时使用context而带来冲突,key建议采用内置类型。
  • 下面的例子我们获取trace_id是直接从当前ctx获取的,实际我们也可以获取父context中的value,在获取键值对时,我们先从当前context中查找,没有找到会再从父context中查找该键对应的值直到在某个父context中返回 nil 或者查找到对应的值。
  • context传递的数据中keyvalue都是interface类型,这种类型编译期无法确定类型,所以不是很安全,所以在类型断言时别忘了保证程序的健壮性。
package main

import (
    "context"
    "fmt"
    "strings"

    "github.com/google/uuid"
)

// 自定义新类型作为ctx的key类型,因为直接用string类型,会有提示:
// should not use built-in type string as key for value; define your own type to avoid collisions
type Tstring string


func NewID() string {
    return strings.Replace(uuid.NewString(), "-", "", -1)
}

func gen(ctx context.Context) chan int {
    dst := make(chan int)
    n := 1
    fmt.Println(ctx.Value(Tstring("traceId")))
    go func() {
        for {
            select {
            // 调用cancel方法时,会触发
            case <-ctx.Done():
                return
            case dst <- n:
                n++
            }
        }
    }()
    return dst
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    ctx = context.WithValue(ctx, Tstring("traceId"), NewID())
    // 调cancel方法来对goroutine进行控制
    cancel()
    for v := range gen(ctx) {
        fmt.Println(v)
        if v == 5 {
            break
        }
    }
}
withvalue+cancel

withCancel取消控制

我们往往为了完成一个复杂的需求会开多个gouroutine去做一些事情,这就导致我们会在一次请求中开了多个goroutine确无法控制他们,这时我们就可以使用withCancel来衍生一个context传递到不同的goroutine中,当我想让这些goroutine停止运行,就可以调用cancel来进行取消。

在上面代码中,通过Background创建了一个ctx并返回了一个cancel函数,通过调用cancel函数来传递信号给goroutine,从而达到对goroutine的控制。

 超时控制

通常健壮的程序都是要设置超时时间的,避免因为服务端长时间响应消耗资源,所以一些web框架或rpc框架都会采用withTimeout或者withDeadline来做超时控制,当一次请求到达我们设置的超时时间,就会及时取消,不在往下执行。withTimeoutwithDeadline作用是一样的,就是传递的时间参数不同而已,他们都会通过传入的时间来自动取消Context,这里要注意的是他们都会返回一个cancelFunc方法,通过调用这个方法可以达到提前进行取消,不过在使用的过程还是建议在自动取消后也调用cancelFunc去停止定时减少不必要的资源浪费。

withTimeoutWithDeadline不同在于WithTimeout将持续时间作为参数输入而不是时间对象,本质withTimout内部也是调用的WithDeadline

package main

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

func gen(ctx context.Context, cancel context.CancelFunc) {
    for i := 0; i < 10; i++ {
        select {
        case <-ctx.Done():
            fmt.Println("该结束了", ctx.Err())
            return
        default:
            fmt.Println("循环i为", i)
            if i == 2 {
                // 协程主动调用函数来取消
                cancel()
            }
            time.Sleep(time.Second)
        }
    }
}

func main() {
    // 超时时间为3秒
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    // 调cancel方法来对goroutine进行控制
    defer cancel()
    // 也可通过传递cancel函数,在协程内主动调用
    go gen(ctx, cancel)
    for i := 0; i < 20; i++ {
        time.Sleep(time.Second)
        fmt.Println("协程个数", runtime.NumGoroutine())
    }
}
超时控制

通过超时控制,既可以达到超时时间终止接下来的执行,也可以没有达到超时时间终止接下来的执行