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
也是key
、value
的形式,为了避免context
因多个包同时使用context
而带来冲突,key
建议采用内置类型。 - 下面的例子我们获取
trace_id
是直接从当前ctx
获取的,实际我们也可以获取父context
中的value
,在获取键值对时,我们先从当前context
中查找,没有找到会再从父context
中查找该键对应的值直到在某个父context
中返回nil
或者查找到对应的值。 context
传递的数据中key
、value
都是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 } } }
withCancel取消控制
我们往往为了完成一个复杂的需求会开多个gouroutine
去做一些事情,这就导致我们会在一次请求中开了多个goroutine
确无法控制他们,这时我们就可以使用withCancel
来衍生一个context
传递到不同的goroutine
中,当我想让这些goroutine
停止运行,就可以调用cancel
来进行取消。
在上面代码中,通过Background创建了一个ctx并返回了一个cancel函数,通过调用cancel函数来传递信号给goroutine,从而达到对goroutine的控制。
超时控制
通常健壮的程序都是要设置超时时间的,避免因为服务端长时间响应消耗资源,所以一些web
框架或rpc
框架都会采用withTimeout
或者withDeadline
来做超时控制,当一次请求到达我们设置的超时时间,就会及时取消,不在往下执行。withTimeout
和withDeadline
作用是一样的,就是传递的时间参数不同而已,他们都会通过传入的时间来自动取消Context
,这里要注意的是他们都会返回一个cancelFunc
方法,通过调用这个方法可以达到提前进行取消,不过在使用的过程还是建议在自动取消后也调用cancelFunc
去停止定时减少不必要的资源浪费。
withTimeout
、WithDeadline
不同在于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()) } }
通过超时控制,既可以达到超时时间终止接下来的执行,也可以没有达到超时时间终止接下来的执行