1 关于context包
在 Go 服务器中,每个传入请求都在其自己的 goroutine 中处理。请求处理程序通常会启动其他 goroutine 来访问后端,例如数据库和 RPC 服务。处理请求的 goroutine 集通常需要访问特定于请求的值,例如最终用户的身份、授权令牌和请求的截止时间。当请求被取消或超时时,处理该请求的所有 goroutine 都应快速退出,以便系统可以回收它们正在使用的任何资源。
而context包,它可以轻松地将请求范围的值、取消信号和截止期限跨 API 边界传递给处理请求所涉及的所有 goroutine。
其实总结起就是一句话:context的作用就是在不同的goroutine之间同步请求特定的数据、取消信号以及处理请求的截止日期。
目前我们常用的一些库都是支持context的,例如gin、database/sql等库都是支持context的,这样更方便我们做并发控制了,只要在服务器入口创建一个context上下文,不断透传下去即可。
2 创建Context
Go提供了两个方法来创建Context实例:
context.Background()
创建默认的上下文,所有其他的上下文都应该从它衍生(Derived)出来。context.TODO()
当暂时不知道使用哪一种上下文时,使用该方法创建上下文来占位。
从后文的源码解析可以知道,这两个方法创建的context是一模一样的,只是官方对其定义不同。
这两种方式创建的都是空的根Context,不具备任何功能。要实现具体功能时,还需要根据需求,从根Context派生具体的Context。(关于Context的派生关系,后文会描述)。
3 使用Context传递数据
3.1 WithValue
使用context.WithValue
方法,可以根据一个Context派生出一个新的子Context,这个新的Context会携带指定的数据。
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
使用context的Value(key any) any
方法,可以从context中获取指定key的数据。
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
- 如果当前context中找到了key,就返回对应值。 如果当前context中没有找到key,就从父context中找,以此类推。 如果在context及其所有父context中都没有找到key,返回nil。
3.2 注意事项
- Context应该只用于跨API或进程传递请求数据,而不应该用于传递可选参数。(可以但不建议)
- key必须是可比较的类型。
- key不应该是字符串或任何其他内置类型,以避免不同包之间在使用上下文时发生键的冲突。
- Context 中存储值的函数通常会在全局变量中分配一个键,然后使用该键作为 context.WithValue 和 Context.Value 的参数。键可以是任何支持相等的类型;包应该将键定义为未导出的类型以避免冲突。
- 定义 Context 键的包应该为使用该键存储的值提供类型安全的访问器。
3.3 官方推荐示例
// User 要存储到context中的值的类型
type User struct {
UserID string
UserName string
}
// key 定义一个未导出的key类型
// 这可以防止与其他包中定义的key发生冲突。
type key int
// userKey context中存储用户信息的key.
// 它也是未导出的
// 客户端使用提供的安全访问器user.NewContext and user.FromContext来访问,而不是直接使用它
var userKey key
// NewContext 返回一个携带用户信息的新context
func NewContext(ctx context.Context, u *User) context.Context {
return context.WithValue(ctx, userKey, u)
}
// FromContext 返回存储在context中的用户信息
func FromContext(ctx context.Context) (*User, bool) {
u, ok := ctx.Value(userKey).(*User)
return u, ok
}
func main() {
c := context.Background()
// 存储用户信息
c = NewContext(c, &User{
UserID: "001",
UserName: "admin",
})
// 读取用户信息
user, exist := FromContext(c)
if exist {
log.Printf("%+v", user)
return
}
log.Println("user not found.")
}
3.4 笔者示例
type requestKeyType string // 请求Key类型
type businessKeyType string // 业务Key类型
const (
traceIDRequestKey requestKeyType = "traceID" // 请求跟踪号
userNameRequestKey requestKeyType = "admin" // 请求用户
traceIDBusinessKey businessKeyType = "traceID" // 业务跟踪号
userNameBusinessKey businessKeyType = "admin" // 业务用户
)
func main() {
// 创建根Context
c := context.Background()
// 携带请求数据
c = context.WithValue(c, traceIDRequestKey, "das454asd564a6s1da165d4a1a3s1d6a413")
c = context.WithValue(c, userNameRequestKey, "admin")
// 携带业务数据
c = context.WithValue(c, traceIDBusinessKey, "das556da1d35de31as1d65a1da4ew15q46q")
c = context.WithValue(c, userNameBusinessKey, "genAdmin")
// 请求处理
DoRequest(c)
// 业务处理
DoBusiness(c)
}
func DoRequest(c context.Context) {
fmt.Println(c.Value(traceIDRequestKey)) // das454asd564a6s1da165d4a1a3s1d6a413
fmt.Println(c.Value(userNameRequestKey)) // admin
}
func DoBusiness(c context.Context) {
fmt.Println(c.Value(traceIDBusinessKey)) // das556da1d35de31as1d65a1da4ew15q46q
fmt.Println(c.Value(userNameBusinessKey)) // genAdmin
}
示例中,我们遵循官方的建议,自定义了key的类型,避免了key的冲突。在上述的场景中,如果我们不自定义key类型,而是直接使用string,就会因为key冲突从而导致程序允许不正确。
4 使用Context实现超时控制
4.1 WithDeadline
WithDeadline返回一个父context的副本,这个副本的deadline被调整到参数d之前(如果父context的deadline在d之前,则取父context的deadline。否则取参数d)。
返回的子context的Done通道将在以下任一场景关闭(谁先发生谁触发):
- 时间到达deadline
- 返回的cancel方法被调用
- 父context的Done通道被关闭。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
return WithDeadlineCause(parent, d, nil)
}
4.2 WithDeadline使用示例
执行一个比较耗时的函数,并设置超时时间为5秒。如果5秒内执行完成,就打印执行结果,否则打印执行超时错误。
func main() {
// 设置超时时间为5秒钟
ctx, cancelFunc := context.WithDeadline(context.Background(), time.Now().Add(time.Second*5))
defer cancelFunc()
// resultChan 结果通道
resultChan := make(chan int,1)
// 异步执行计算方法
go func() {
resultChan <- cal(5)
}()
// 等待
select {
case <-ctx.Done():
// 打印超时信息
fmt.Println(ctx.Err()) // 最终输出了:context deadline exceeded
fmt.Println(context.Cause(ctx)) // 最终输出了:context deadline exceeded
case n := <-resultChan:
// 打印结果信息
fmt.Println(n)
}
}
// cal 方法睡眠6秒后返回
func cal(n int) int {
time.Sleep(time.Second * 6)
return n * n
}
4.3 WithDeadlineCause
再看一眼WithDeadline方法的源码:
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
return WithDeadlineCause(parent, d, nil)
}
可以看到,WithDeadline方法实际上就是调用WithDeadlineCause方法,只是没有设置最后一个cause参数而已。
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
deadline: d,
}
c.cancelCtx.propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, cause)
})
}
return c, func() { c.cancel(true, Canceled, nil) }
}
这个cause参数,实际上是用于指定【原因】。调用context的Cause方法,可以获取到context被取消的原因。
cause参数
现在假定一个带超时时间或者截止时间的context(加入变量名为ctx),因为到达指定的时间,被自动超时取消了:
- 如果创建时指定了cause参数为timeout
- 调用
ctx.Err()
方法,将返回context deadline exceeded
- 调用
context.Cause(ctx)
方法,将返回timeout
- 调用
- 如果创建时没有指定cause
- 调用
ctx.Err()
方法,将返回context deadline exceeded
- 调用
context.Cause(ctx)
方法,将返回context deadline exceeded
- 调用
一句话来说:WithDeadline和WithDeadlineCause的区别在于有无cause,cause用于指定超时后context.Cause()方法返回的值。这看起来好像作用不大~
4.4 WithDeadlineCause使用示例
本示例就在WithDeadline示例的基础上进行少量改造:
func main() {
// 设置超时时间为5秒钟
// 设置超时原因为timeout
ctx, cancelFunc := context.WithDeadlineCause(context.Background(), time.Now().Add(time.Second*5), errors.New("timeout"))
defer cancelFunc()
// resultChan 结果通道
resultChan := make(chan int, 1)
// 异步执行计算方法
go func() {
resultChan <- cal(5)
}()
// 等待
select {
case <-ctx.Done():
// 打印超时信息
fmt.Println(ctx.Err()) // 最终输出了:context deadline exceeded
fmt.Println(context.Cause(ctx)) // 最终输出了:timeout
case n := <-resultChan:
// 打印结果信息
fmt.Println(n)
}
}
// cal 方法睡眠6秒后返回
func cal(n int) int {
time.Sleep(time.Second * 6)
return n * n
}
4.5 WithTimeout & WithTimeoutCause
源码:
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc) {
return WithDeadlineCause(parent, time.Now().Add(timeout), cause)
}
可以看到,WithTimeout、WithTimeoutCause实际上分别是WithDeadline、WithDeadlineCause方法的简单变形。对于调用者来说,只是传参方式不一样而已,其他地方完全一致。因此这里不再赘述。
5 使用Context取消goroutine执行
5.1 WithCancel
有时候为了完成一个复杂的需求会开多个gouroutine去做一些事情,这就导致我们会在一次请求中开了多个goroutine却无法控制他们,这时我们就可以使用withCancel来衍生一个context传递到不同的goroutine中,当我想让这些goroutine停止运行,就可以调用cancel来进行取消。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
WithCancel 返回带有新 Done 通道的父级副本。当调用返回的取消函数或关闭父context的 Done 通道(以先发生者为准)时,返回的context的 Done 通道将关闭。
5.2 WithCancel使用示例
在下面的示例中,我们将启动一个协程去不断生成随机数。主协程负责读取生成的随机数,并在生成的随机数等于0时,取消随机数的生成。
func main() {
// 创建取消上下文
ctx, cancelFunc := context.WithCancel(context.Background())
// 启动一个协程去生成随机数
c := genRandomNum(ctx)
// 读取生成的随机数
for {
i := <-c
log.Printf("receive %d\n", i)
// 如果生成的随机数等于0,就取消随机数生成
if i == 0 {
cancelFunc()
break
}
}
log.Println("end")
// 这是为了等待被取消的协程打印日志,正常是不需要的
time.Sleep(time.Second)
}
// genRandomNum 用于不断生成随机数,知道上下文取消
func genRandomNum(ctx context.Context) <-chan int {
res := make(chan int)
go func() {
for {
select {
case <-ctx.Done():
log.Println(ctx.Err()) // 将打印:context canceled
return
case res <- rand.Intn(100):
}
}
}()
return res
}
5.3 WithCancelCause
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
WithCancelCause
方法与WithCancel
方法唯一的不同在于:WithCancelCause
方法返回的取消方法可以指定取消的原因。
使用context.Cause方法可以获取取消原因。
5.4 WithCancelCause使用示例
在下面的示例中,我们将启动一个协程去不断生成随机数。主协程负责读取生成的随机数,并在生成的随机数等于0,或者生成的随机数数量达到100时,取消随机数的生成,同时设置取消的原因。
func main() {
// 创建取消上下文
ctx, cancelFunc := context.WithCancelCause(context.Background())
// 启动一个协程去生成随机数
c := genRandomNum(ctx)
count := 0
// 读取生成的随机数
for {
i := <-c
log.Printf("receive %d\n", i)
// 如果生成的随机数等于0,就取消随机数生成
if i == 0 {
cancelFunc(errors.New("generated num is zero"))
break
}
// 如果生成的随机数数量达到100个,就取消随机数生成
count++
if count == 100 {
cancelFunc(errors.New("the number of generated numbers equals 100"))
break
}
}
log.Println("end")
// 这是为了等待被取消的协程打印日志,正常是不需要的
time.Sleep(time.Second)
}
// genRandomNum 用于不断生成随机数,知道上下文取消
func genRandomNum(ctx context.Context) <-chan int {
res := make(chan int)
go func() {
for {
select {
case <-ctx.Done():
log.Println(ctx.Err()) // 总会打印:context canceled
// 可能打印:generated num is zero
// 可能打印:the number of generated numbers equals 100
log.Println(context.Cause(ctx))
return
case res <- rand.Intn(100):
}
}
}()
return res
}
6 源码阅读
6.1 Context核心接口
contex包的核心是Context类型:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
- Deadline 返回此 Context 将被取消的时间(如果有),也就是截至时间。
- 如果没有设置截至时间,返回的ok=false
- Done 返回一个通道,该通道充当代表 Context 运行的函数的取消信号,该通道在Context被取消或者超时时关闭。
- 如果Context永远不会被取消/超时,该方法将返回一个nil通道
- WithCancel会使Done通道在被取消时关闭;WithDeadline会使Done通道在到达截至时间时关闭;WithTimeout会使Done通道在超时时关闭。
- Err方法返回Done通道关闭的原因
- 如果Done通道没有关闭,返回nil
- 如果Done通道关闭了,Err返回一个非空的error来解释原因
- 如果是因为Context被取消了,返回Canceled
- 如果是因为到达截止时间,返回DeadlineExceeded
- 当Err返回一个非空的报错后,后续都返回相同的报错。
- Value 返回与 key 关联的值,如果没有则返回 nil。
- Value 允许 Context 携带请求范围的数据。该数据对于多个 goroutine 同时使用必须是安全的。
- Context的Value应该用于跨API和进程边界传递请求范围的值,而不应该用于在方法中传递可选参数。
- key标识Context中的特定值。在 Context 中存储值的函数通常会在全局变量中分配一个键,然后使用该键作为 context.WithValue 和 Context.Value 的参数。键可以是任何支持相等的类型;包应该将键定义为未导出的类型以避免冲突。
- 定义 Context 键的包应该为使用该键存储的值提供类型安全的访问器。
6.2 Context的派生
对于一个Context实例,可以使用go语言提供的With系列的方法进行派生,即根据传入的父节点Context,派生一个子节点Context。每个父节点Context可以派生出任意数量的子节点Context,这样就形成了一个Context树:
6.3 根Context
Go源码中提供了两个根Context:
- backgroundContext
- 是一个非零、空的Context,永远不会被取消,也没有Values,也没有超时时间。它通常在main函数、初始化和测试时使用。
- 通常会将它作为顶级上下文。
- todoContext
- todoContext实际上跟background是一模一样的,从后面的源码中也可以看出来。
- 当不清楚要使用哪个 Context 或它尚不可用时(因为周围的函数尚未扩展为接受 Context 参数),代码应使用 context.TODO。
emptyCtx
emptyCtx 提供了Context接口的空实现:永远不会被取消,也没有Values,也没有超时时间
type emptyCtx struct{}
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 any) any {
return nil
}
backgroundCtx
从源码可以看到,backgroundCtx 实际上就是一个空Context,只不过提供了一个Background方法来创建实例。
type backgroundCtx struct{ emptyCtx }
func (backgroundCtx) String() string {
return "context.Background"
}
func Background() Context {
return backgroundCtx{}
}
todoCtx
从源码可以看到,todoCtx和backgroundCtx是一模一样的。【唯一的不同也就是官方对他们两个使用场景的定义了吧】
type todoCtx struct{ emptyCtx }
func (todoCtx) String() string {
return "context.TODO"
}
func TODO() Context {
return todoCtx{}
}
6.4 派生方法
WithCancel
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
WithCancel方法返回两个值,第一个是基于父Context派生的cancelCtx,第二个是取消方法CancelFunc。
- 返回的cancelCtx是一个带有新 Done 通道的父级Context副本。当调用返回的CancelFunc,或者父级Context的Done通道被关闭时,它的Done通道也会被关闭。
- 取消Context会释放其关联的资源,因此代码应该在Context中的操作完成时,立即调用取消方法。
- 取消一个Context时,其实现了canceler接口的所有子节点Context也会被取消。
WithCancelCause
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
WithCancelCause 的行为类似于 WithCancel,但返回 CancelCauseFunc而不是 CancelFunc。CancelCauseFunc 的行为类似于 CancelFunc,但还设置取消原因。可以通过在已取消的上下文或其任何派生上下文上调用 Cause来检索此原因。
Cause 返回一个非零错误,解释 被取消的原因。 Context或其父级之一的第一次取消确定了原因。如果取消是通过调用 CancelCauseFunc(err) 发生的,则 [Cause] 返回 err。否则 Cause© 返回与 c.Err() 相同的值。如果 c 尚未取消,Cause 返回 nil。
func Cause(c Context) error {
if cc, ok := c.Value(&cancelCtxKey).(*cancelCtx); ok {
cc.mu.Lock()
defer cc.mu.Unlock()
return cc.cause
}
return c.Err()
}
WithoutCancel
WithCancel 返回带有新 Done 通道的父级副本。当调用返回的取消函数或关闭父上下文的 Done 通道(以先发生者为准)时,返回的上下文的 Done 通道将关闭。
取消此上下文会释放与其关联的资源,因此在此上下文中运行的操作完成后,代码应立即调用取消。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
WithDeadline
ithDeadline 返回父上下文的副本,并将截止日期调整为不晚于 d。如果父级的截止日期已经早于 d,则 WithDeadline(parent, d) 在语义上等同于parent。当截止时间到期、调用返回的取消函数时或当父上下文的 Done 通道关闭时(以先发生者为准),返回的 [Context.Done] 通道将关闭。
取消此上下文会释放与其关联的资源,因此在此 [Context] 中运行的操作完成后,代码应立即调用取消。
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
return WithDeadlineCause(parent, d, nil)
}
WithDeadlineCause
WithDeadlineCause 的行为类似于 [WithDeadline],但还设置超出截止日期时返回 Context 的原因。返回的[CancelFunc]没有设置原因。
func WithDeadlineCause(parent Context, d time.Time, cause error) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
c := &timerCtx{
deadline: d,
}
c.cancelCtx.propagateCancel(parent, c)
dur := time.Until(d)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, cause) // deadline has already passed
return c, func() { c.cancel(false, Canceled, nil) }
}
c.mu.Lock()
defer c.mu.Unlock()
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, cause)
})
}
return c, func() { c.cancel(true, Canceled, nil) }
}
WithTimeout
WithTimeout 返回 WithDeadline(parent, time.Now().Add(timeout))。
取消此上下文会释放与其关联的资源,因此在此 [Context] 中运行的操作完成后,代码应立即调用取消
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithTimeoutCause
WithTimeoutCause 的行为类似于 [WithTimeout],但也设置超时到期时返回 Context 的原因。返回的[CancelFunc]没有设置原因。
func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc) {
return WithDeadlineCause(parent, time.Now().Add(timeout), cause)
}
WithValue
WithValue 返回父级的副本,其中与 key 关联的值为 val。仅将上下文值用于传输进程和 API 的请求范围数据,而不是用于将可选参数传递给函数。提供的键必须是可比较的,并且不应是字符串类型或任何其他内置类型,以避免使用上下文的包之间发生冲突。 WithValue 的用户应该定义自己的键类型。为了避免在分配给 interface{} 时进行分配,上下文键通常具有具体类型 struct{}。或者,导出的上下文键变量的静态类型应该是指针或接口。
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}