golang中context上下文的使用方法以及实现原理
序言
本文通过编写使用context的测试案例,并简要介绍了少量接口源码,简单阐述了Golang中context上下文的使用方法、适用场景及其实现机制。
1. 什么是context上下文
什么是上下文,顾名思义,上下文就是上文和下文,能够将上文和下文数据串连起来的一种机制,golang中的上下文就是指API之间或者方法调用之间所传递的业务参数之外的额外信息,比如客户端ip地址、请求接收的时间、客户端身份信息以及函数的超时机制信息等。
2. 使用方法及原理
2.1 基本方法
golang中的context提供两个生成顶层context的方法 1.context.BackBround():返回一个空的context不会被取消的context,一般用在主函数、初始化以及创建根的时候。 2.context.TODO():和context.BackBround()方法一样,一般用在不确定传递什么上下文信息的时候使用,和第一个方法的底层实现其实是一模一样的,名字不同只是为了提高程序的可读性。 Context接口中context使用的一些方法:
type Context interface {
Deadline() (deadline time.Time, ok bool)// 返回context的截止日期
Done() <-chan struct{}//返回一个channel对象context被取消时此channel会被关闭
Err() error//当context被取消时,可调用Err获取报错信息或context被取消的原因
Value(key any) any//返回此context指定key对应的value
}
在使用context的时候有几个约定俗成的规则: 1.函数创建时,就要把context放在第一个参数的位置 2.不要使用nil充当context类型的参数值,可以使用context.Backgroud创建一个 3.context中key的类型最好不要使用string,最好使用空接口或自定义别名,保证传递过程中,其他人使用到你的上下文时不会发生key的冲突即可。 例如:
// 定义一个空结构体类型作为键的基础类型
type contextKey struct{} // 定义一个键实例,导出为接口类型
var UserKey interface{} = contextKey{}
func main() {
// 创建一个带有值的上下文
ctx := context.WithValue(context.Background(), UserKey, "Alice")
// 从上下文中检索值
if user, ok := ctx.Value(UserKey).(string); ok {
fmt.Println("User:", user)
} else {
fmt.Println("User not found")
}
}
2.2 特殊方法
context标准库中还有一些特殊用来创建context的方法,可以传递父context信息
2.2.1 WithValue()带键值对的上下文
WithValue()方法是基于父context生成一个新的context,而这个新的context底层是一个内嵌了父context的结构体,所以这样就可以使用WithValue方法来实现一条context链,当子context查找某个键没找到还可以去父context中继续查找。 嵌套context和withValue源码
type valueCtx struct {
Context
key, val any// 嵌套的key-value
}
// WithValue接收一个父context,和一个键值对,返回一个子context,内部嵌套了父context
func WithValue(parent Context, key, val any) Context {
//此处panic省略
return &valueCtx{parent, key, val}
}
测试案例:当子context没有某个key,还可以去父context获取
// 测试WithValue方法,查找某个键,子context没有会继续去父context查找
type contextKey1 struct{}
type contextKey2 struct{}
var UserKey1 = &contextKey1{}
var UserKey2 = &contextKey2{}
func main() {
ctx := context.TODO()
ctx = context.WithValue(ctx, UserKey1, "UserKey1")// 父context
ctx = context.WithValue(ctx, UserKey2, "UserKey2")// 子context
fmt.Println(ctx.Value(UserKey1))
}
执行结果:
2.2.2 WithCancel()带取消功能的上下文
WithCancel()接收一个上下文对象,返回一个该上下文的副本和一个cancel方法,调用cancel可以取消原来的上下文,这里返回的上下文对象其实也是一个cacelCtx取消上下文类型,和上面的valueCtx类似,它实现了原生的context接口,并且添加了与cancel取消机制有关的结构 cancelCtx取消续上下文源码:
type cancelCtx struct {
Context//实现原生的context接口
mu sync.Mutex // 保护以下字段
done atomic.Value // chan 类型,延迟创建,在第一次取消调用时关闭
children map[canceler]struct{} // 管理取消上下文的子接口
err error // 记录错误信息
cause error
}
测试案例:创建一个带取消功能的上下文,可以定时主动取消
func main() {
// 创建一个带有取消功能的上下文和取消函数
ctx, cancel := context.WithCancel(context.Background())
// 启动一个并发任务,传递上下文
go doWork(ctx)
// 模拟一段时间后取消操作
time.Sleep(1 * time.Second)
cancel() // 取消操作
// 等待任务完成
time.Sleep(1 * time.Second)
fmt.Println("Main goroutine completed")
}
func doWork(ctx context.Context) {
// 模拟一些工作
for {
select {
case <-ctx.Done():
fmt.Println("Worker goroutine received cancel signal")
return
default:
// 模拟一些工作
time.Sleep(500 * time.Millisecond)// 半秒打印一次
fmt.Println("Working...")
}
}
}
结果:
2.2.3 WithCancel 和 WithValue使用注意
带父子关系的上下文链路,当父context被取消了之后,父上下文的取消信号通过内部的 done
通道传播给子上下文,所以子context同样也会立即被取消
测试案例:
func main() {
// 创建一个根上下文
rootCtx := context.Background()
// 使用 WithCancel 创建一个取消上下文和取消函数
ctx, cancel := context.WithCancel(rootCtx)
defer cancel()
// 在根上下文中添加一个值
ctxWithValue := context.WithValue(ctx, "key1", "value1")
// 启动一个 goroutine 来处理任务
go func(ctx context.Context) {
// 在子上下文中进行工作
for {
select {
case <-ctx.Done()://接收子上下文取消信号
fmt.Println("Child context received cancel signal")
return
default:
// 获取并打印上下文中的值
value := ctx.Value("key1")
fmt.Printf("Working with value: %v\n", value)
// 模拟一些工作
time.Sleep(500 * time.Millisecond)
}
}
}(ctxWithValue)
// 模拟一段时间后取消根上下文
time.Sleep(1 * time.Second)
cancel()
// 等待 goroutine 执行完成
time.Sleep(500 * time.Millisecond)
fmt.Println("Main goroutine completed")
}
运行结果:可以看到,当父上下文cancel(),子上下文也会接收到消息,也取消了
2.2.3 WithTimeout()和WithDeadline()
这两个方法其实十分接近,都是跟上下文时间有关,只不过 WithTimeout()带是超时时间的上下文,而WithDeadline()是带截止时间的上下文,一个是以起点开始计时,一个是只管时间终点不管时间起点。时间过期或到了指定时间,context会自动取消,用法和上面的两个方法接近。
下节预告:golang中channel通道的使用技巧以及实现原理
转载自:https://juejin.cn/post/7391699542007169033