context包是个啥?
Go 语言的 context 包提供了一种在应用程序中跨多个 goroutine 传递请求特定数据、取消操作和管理超时的机制, 它被广泛用于处理并发和异步操作.
context的四个核心API
-
WithCancel : (返回一个可取消的context实例和取消函数)
WithCancel(parent Context) (ctx Context, cancel CancelFunc)
创建一个可以取消的子 Context。当调用返回的 cancel 函数时,与该 Context 关联的所有子 Context 都会被取消, 如果不调用cancel函数,将会一直阻塞!!
-
WithDeadline : (返回一个可取消的context实例和取消函数)
WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
创建一个带有截止时间的子 Context。当截止时间过期时,与该 Context 关联的操作将自动取消。可以设置过期时间,让context自动过期,也可以不等待过期时间,手动调用cancel函数取消
-
WithTimeout : (返回一个可取消的context实例和取消函数)
WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
创建一个具有超时时间的子 Context。在指定的超时时间之后,与该 Context 关联的操作将自动取消。和withDeadLine一样,可以设置过期时间,让context自动过期,也可以不等待过期时间,手动调用cancel函数取消,区别在于函数的传参不同.
-
WithValue : (设置键值对,并且返回一个新的context实例)
WithValue(parent Context, key interface{}, value interface{})
创建一个通过键值对传递数据的子 Context。这些数据可以通过 Value 方法在 Context 树中的任何级别进行访问。
注意 : context实例是不可变的,每一次都是新创建的。
context包常用来干啥
每个 Context 实例都可以传递给 Go 协程,并且可以随着协程的执行传递和取消。通过使用 Context,可以实现以下操作:
- 跨多个 goroutine 传递请求特定的值,如请求 ID、用户身份验证信息等。
- 实现请求的超时控制,以避免长时间运行的操作阻塞系统。
- 在并发操作中进行协调,例如控制同时进行的最大请求数或取消正在进行的请求。
context使用注意
在使用 Context 时,需要遵循以下最佳实践:
- 将 Context 作为函数的第一个参数传递,并将其命名为 ctx。
- 使用 context.Background() 作为顶级 Context,它是所有其他 Context 的父级。
- 当一个操作完成或某些条件达到时,调用 cancelFn() 函数来取消与 Context 关联的所有操作。
- 在 Goroutine 中使用 select 语句监听 Context 的取消信号,以便及时退出。
四个核心API的基本使用
context.WithValue() 的使用
func TestContext_withValue(t *testing.T) {
ctx := context.Background() // 一般是链路起点和调用起点,创建根context,常用
//ctx := context.TODO() // 在不确定context该用啥的时候,用这个,不常用
ctx = context.WithValue(ctx, key{}, "i am value") // 社区推荐
//ctx = context.WithValue(ctx, "key", "value") // 这样也可以...
val := ctx.Value(key{})
t.Log("val:", val)
v2 := ctx.Value("不存在的key")
// 不确定设置的value是否是string或者其他类型的时候,这里断言一下
valTwo, ok := v2.(string)
if !ok {
t.Log("类型错误")
return
}
t.Log("valTwo:", valTwo)
}
context.WithCancel() 如果不调用cancel函数,将会一直阻塞!!
func TestContext_withCancel(t *testing.T) {
ctx := context.Background() // 一般是链路起点和调用起点,创建根context,常用
ctx, cancel := context.WithCancel(ctx)
// 使用完ctx之后,调用cancel,结束整条调用
//defer cancel()
// 1秒之后结束调用
go func() {
time.Sleep(time.Second * 1)
cancel()
}()
// 调用其他函数或者协程,传递ctx
t.Log("start...")
<-ctx.Done() // 上面不调用 cancel(), 此处将会一直阻塞
t.Log("end...", ctx.Err())
}
context.WithDeadline() 可以设置过期时间,让context自动过期,也可以不等待过期时间,手动调用cancel函数取消
func TestContext_withDeadline(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second*3)) // 3秒后过期
// deadLineTime:过期时间,isHasTimeout: 是否设置了过期时间
deadLineTime, isHasTimeout := ctx.Deadline()
if isHasTimeout {
t.Log("设置了过期时间,过期时间为:", deadLineTime)
}
//defer cancel() // 等待3秒过期后,进行关闭
// 2秒之后结束调用
go func() {
time.Sleep(time.Second * 2) // 不等待设置的3秒过期时间,手动结束
cancel()
}()
<-ctx.Done() // 这里不会一直阻塞,因为设置了3秒后过期
t.Log("end...", ctx.Err())
}
context.WithTimeout() 和withDeadLine一样,可以设置过期时间,让context自动过期,也可以不等待过期时间,手动调用cancel函数取消,区别在于函数的传参不同.
func TestContext_withTimeout(t *testing.T) {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, time.Second*3) // 3秒后过期
// timeOutTime:过期时间,isHasTimeout: 是否设置了过期时间
timeOutTime, isHasTimeout := ctx.Deadline()
if isHasTimeout {
t.Log("设置了过期时间,过期时间为:", timeOutTime)
}
//defer cancel() // 等待3秒过期后,进行关闭
// 2秒之后结束调用
go func() {
time.Sleep(time.Second * 2) // 不等待设置的3秒过期时间,手动结束
cancel()
}()
<-ctx.Done() // 这里不会一直阻塞,因为设置了3秒后过期
t.Log("end...", ctx.Err())
}
context的父子关系
控制是从上至下
当父context取消或者超时的时候, 所有由它派生的子context都会取消或超时
查找value是从下至上
当使用key查找value的时候,先看自己有没有,没有的话往祖先上查找
上代码
func TestContext_Parent(t *testing.T) {
ctx := context.Background()
parent := context.WithValue(ctx, "key", "parent-value")
childOne := context.WithValue(parent, "key", "childOne-value")
// 相同的key子context的如果设置了value,会把父context传下来的值覆盖掉
t.Log("parent-key:", parent.Value("key")) // parent-key: parent-value
t.Log("childOne-key:", childOne.Value("key")) // childOne-key: childOne-value
childTwo, cancel := context.WithTimeout(parent, time.Second)
defer cancel()
// 子context没有设置自己的value,还是沿用父context的value
t.Log("childTwo-key:", childTwo.Value("key")) // childTwo-key: parent-value
// 父context无法拿到子context设置的值
childThree := context.WithValue(parent, "new-key", "childThree-value")
t.Log("parent-new-key:", parent.Value("new-key")) // parent-new-key: <nil>: <nil>, 父context拿不到childThree设置的 new-key
t.Log("childThree-new-key:", childThree.Value("new-key")) // childThree-new-key: childThree-value
// 如果父context一定要拿到子context中设置的值,可以传一个map下去,子孙们往里面装数据,这样能拿到
parentTwo := context.WithValue(ctx, "parentTwo-key", map[string]string{})
ptChildOne, cancel := context.WithTimeout(parentTwo, time.Second)
defer cancel()
// 拿到context中的map
m := ptChildOne.Value("parentTwo-key").(map[string]string)
// 在子context中给map中设置一个数据
m["key1"] = "value1"
// 从父context中取出这个map
ptm := parentTwo.Value("parentTwo-key")
// 如果不确定类型,先断言一下
v, ok := ptm.(map[string]string)
if !ok {
t.Log("类型错误")
return
}
// 打印 parentTwo-key: map[key1:value1], 这样的话父context就可以拿到子context中设置的数据
t.Log("parentTwo-key: ", v)
}