李林超博客
首页
归档
留言
友链
动态
关于
归档
留言
友链
动态
关于
首页
GO
正文
golang之context介绍
Leefs
2024-01-26 PM
1248℃
0条
[TOC] ### 一、Context作用 `context.Context`是Go中定义的一个接口类型,从1.7版本中开始引入。 其主要作用是在一次请求经过的所有**协程或函数间传递取消信号**及**共享数据**,以达到**父协程对子协程**的**管理**和**控制**的目的。 需要注意的是`context.Context`的作用范围是**一次请求的生命周期**,即随着请求的产生而产生,随着本次请求的结束而结束。 ![03.golang之context介绍01.png](https://lilinchao.com/usr/uploads/2024/01/3395997342.png) ### 二、基本数据结构 在context包中,context.Context的定义实际上是一个**接口类型**,该接口定义了获取上下文的Deadline的函数,根据key获取value值的函数、还有获取done通道的函数。 ```go type Context interface { // DealLine: 返回context的过期时间 Deadline() (deadline time.Time, ok bool) // Done:返回context中的chan Done() <-chan struct{} // Err:返回错误 Err() error // Value:返回context中存储对应key的值 Value(key any) any } ``` **说明** + **Deadline方法**: 返回 context 是否会被取消以及自动取消的截止时间 + 第一个返回值是截止时间,到了这个时间点,Context 会自动发起取消请求; + 第二个返回值 `ok==false` 时表示没有设置截止时间,如果需要取消的话,需要调用取消函数进行取消。 + **Done方法**:返回一个只读的 chan,类型为 struct{} + 如果该方法返回的 chan 可以读取,那么就说明 parent context 已经发起了取消请求,当我们通过 Done 方法收到这个信号后,就应该做清理操作,然后退出 goroutine,释放资源。 之后,Err 方法会返回一个错误,告知为什么 Context 被取消。 + **Err方法**:返回取消的错误原因,因为什么 Context 被取消。 + **Value方法**:获取该 Context 上保存的键值对,所以要通过一个 Key 才可以获取对应的值,这个值一般是线程安全(并发安全)的。 + 虽然 context 是一个并发安全的类型,但是如果 context 中保存着 value,则这些 value 通常不是并发安全的,并发读写这些 value 可能会造成数据错乱,严重的情况下可能发生 panic。 + 在并发时,如果我们的业务代码需要读写 context 中的 value,那么最好建议 clone 一份原来的 context 中的 value,并塞到新的 ctx 传递给各个gorouinte。 当然, 如果已经明确不会有并发读取,那么可以直接使用,或者使用的时候加锁。 ### 三、接口实现 从上文可以得知Context本质是一个接口类型。接口类型是需要通过具体的结构体进行实现的。 在context包中已经定义好了所需场景的结构体,这些结构体已经实现了Context接口的方法,在项目中就可以直接进行使用。 在context包中定义有`emptyCtx`、`cancelCtx`、`timerCtx`、`valueCtx`四种结构体。 其中`cancelCtx`、`timerCtx`实现了给子协程传递取消信号。`valueCtx`结构体实现了父协程和子协程传递共享数据相关。 **不同功能的Context实例** ```go // 最基础的实现,也可以叫做父 context func Background() Context func TODO() Context //创建带有取消功能的Context func WithCancel(parent Context) (ctx Context, cancel CancelFunc) //创建带有定时自动取消功能的Context func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) //创建带有定时自动取消功能的Context func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) func WithValue(parent Context, key interface{}, val interface{}) Context ``` **说明** + `WithCancel` 函数,传递一个 parent Context 作为参数,返回子 Context,以及一个取消函数用来取消 Context。 前面说到控制父子协程的生命周期,就可以通过这个函数来实现。 + `WithDeadline` 函数,和 `WithCancel` 差不多,但是它会多传递一个截止时间参数,这样的话,当到了截止的时间点,就会自动取消 Context。 当然也可以不等到这个时候,然后可以通过取消函数提前进行取消。 + `WithTimeout` 函数,和 `WithDeadline` 基本上一样,会传入一个 timeout 超时时间,也就是从现在开始,直到过来 timeout 时间后,就进行超时取消,注意,这个是超时取消,不是截止时间取消。 + `WithValue` 函数,这个和 `WithCancel` 就没有关系了,它不是用来控制父子协程生命周期的,是通过在 context 中传递基础元数据用的。 这个可以在 context 中存储键值对的数据,然后这个键值对的数据可以通过 `Context.Value` 方法获取到。一般想要通过上下文来传递数据时,可以通过这个方法。 对应的函数创建的结构体及该实例所实现的功能的主要特点如下图所示: ![03.golang之context介绍02.jpg](https://lilinchao.com/usr/uploads/2024/01/3151639708.jpg) 在图中可以看到结构体依次是继承关系。因为在`cancelCtx`结构体内嵌套了Context(实际上是`emptyCtx`)、`timerCtx`结构体内嵌套了`cancelCtx`结构体,可以认为它们之间存在继承关系。 通过`WithTimeout`和`WithDealine`函数创建的Context实际上都是`timerCtx`结构体,唯一的区别就是`WithDeadline`函数的第二个参数指定的是最后的时间点,而`WithTimeout`函数的第二个参数是一段时间。但`WithDealine`在内部实现中本质上也是将时间点转换成距离当前的时间段。 ### 四、常用方法实例 #### 4.1 传递请求范围数据 **代码示例** ```go import ( "context" "fmt" ) // 定义一个键类型(key)用于context中的数据传递 type key string // 在 context 中设置数据 func WithValue(ctx context.Context){ // 通过 WithValue 将数据存储在 context 中 ctxWithData := context.WithValue(ctx,key("name"),"李林超博客!") // 调用另一个函数,并将带有数据的 context 传递给它 printName(ctxWithData) } // 从 context 中获取并使用数据 func printName(ctx context.Context){ // 从context中获取数据,并进行类型断言 if name, ok := ctx.Value(key("name")).(string); ok { fmt.Println("Name:", name) } } func main() { // 创建根 context ctx := context.Background() // 传递 context 并设置数据 WithValue(ctx) } ``` **运行结果** ``` Name: 李林超博客! ``` **分析** 在上面的示例中,定义了一个key类型,用于在context中存储数据。 然后,使用`WithValue`函数将数据存储在带有数据的`context ctxWithData` 中,并将其传递给`printName`函数。 在`printName`函数中,使用Value方法从context中获取数据,并进行类型断言后打印出来。 #### 4.2 取消信号 **代码示例** ```go import ( "context" "fmt" "time" ) // 模拟一些耗时操作 func performTask(ctx context.Context){ // 检查是否接收到取消信号 select { case <-ctx.Done(): fmt.Println("Task canceled") return default: // 模拟长时间运行的任务 time.Sleep(5 * time.Second) fmt.Println("Task completed") } } func main() { // 创建根 context ctx := context.Background() // 派生子 context,并设置取消信号 ctx, cancel := context.WithCancel(ctx) // go performTask(ctx) // 模拟一些操作后取消任务 time.Sleep(2 * time.Second) cancel() // 发送取消信号 // 启动耗时操作的 goroutine,并传递带有取消信号的 context go performTask(ctx) // 等待一段时间,确保程序有足够的时间处理取消信号 time.Sleep(1 * time.Second) } ``` **运行结果** ``` Task canceled ``` **分析** 在上面的示例中,创建了一个任务函数`performTask`,该函数会检查是否接收到取消信号。 使用`context.WithCancel`函数创建派生的子context,并通过调用返回的`cancel`函数发送取消信号。 然后,在一个`goroutine`中运行任务函数,并通过传递带有取消信号的context来监听取消信号。 在主`goroutine`中,需要等待一段时间后调用`cancel`函数发送取消信号。 当任务函数接收到取消信号后,它会打印"Task canceled"。 #### 4.3 超时处理 **代码示例** ```go import ( "context" "fmt" "time" ) // 模拟一些耗时操作 func performTask(ctx context.Context) { // 检查是否接收到取消信号或超时 select { case <-ctx.Done(): fmt.Println("Task canceled") case <-time.After(5 * time.Second): fmt.Println("Task completed") } } func main() { // 创建根context ctx := context.Background() // 派生子context,并设置超时时间 ctx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() // 启动耗时操作的goroutine,并传递带有超时设置的context go performTask(ctx) // 等待一段时间,确保程序有足够的时间处理超时或取消信号 time.Sleep(5 * time.Second) } ``` **运行结果** ``` Task canceled ``` **分析** 在上面的示例中,创建了一个任务函数`performTask`,该函数会检查是否接收到取消信号或超时。 使用`context.WithTimeout`函数创建派生的子context,并通过调用返回的`cancel`函数来设置超时时间。 然后,在一个`goroutine`中运行任务函数,并传递带有超时设置的context来监听超时或取消信号。 在主`goroutine`中,需要等待一段时间以确保程序有足够的时间处理超时或取消信号。当超过超时时间后,任务函数会打印"Task canceled"。 ### 五、context使用注意事项 在使用context时,有一些需要注意的事项,以及一些与性能优化相关的建议: + **避免滥用context传递数据**:context的主要目的是传递请求范围的数据和取消信号,而不是用于传递全局状态或大量数据。滥用context传递大量数据可能导致上下文对象变得臃肿,增加内存和GC压力。 + **不要修改已传递的context**:传递的context是不可变的,即使在函数内部对其调用cancel方法也不会影响调用方的context。如果需要对context进行修改,应该通过返回一个新的派生context来实现。 + **只在需要时传递context**:不要将context作为函数参数无限制地传递,而是在需要时传递。这样可以避免不必要的复杂性和代码膨胀。 + **及早检查取消信号**:在使用context的地方,应该及早检查`ctx.Done()`的返回值,以尽早响应取消信号。在耗时操作前或可能阻塞的地方,应该通过select语句来监听多个操作,包括取消信号、超时和其他channel。 + **使用WithCancel替代WithTimeout**:在可能的情况下,优先使用WithCancel函数来设置取消信号,而不是仅仅依赖于WithTimeout函数。这样可以有更精确的控制和更灵活的处理方式。 + **优化context的传递**:在频繁调用的函数链中,避免在每个函数中重复传递相同的context,可以通过使用结构体或函数闭包将context作为参数进行传递,从而减少代码重复和提升性能。 + **及时取消不再需要的goroutine**:如果在多个goroutine中使用context,确保在不再需要时及时取消goroutine,以避免资源浪费和潜在的goroutine泄漏。 这些注意事项和性能优化建议可帮助确保正确且高效地使用context,避免滥用和性能问题。根据具体场景和需求,可以灵活使用context的机制来优化代码的可读性、并发安全性和性能。 *附参考文章链接* *https://cloud.tencent.com/developer/article/2236941* *https://blog.csdn.net/xiangzaixiansheng/article/details/134399568*
标签:
Golang
非特殊说明,本博所有文章均为博主原创。
如若转载,请注明出处:
https://lilinchao.com/archives/2876.html
上一篇
go并发之channel底层实现原理
下一篇
golang基础使用示例
取消回复
评论啦~
提交评论
栏目分类
随笔
2
Java
326
大数据
229
工具
31
其它
25
GO
47
标签云
设计模式
Stream流
Java工具类
序列化和反序列化
线程池
Livy
SpringCloudAlibaba
容器深入研究
Jquery
MyBatis-Plus
机器学习
锁
Redis
HDFS
Azkaban
稀疏数组
SQL练习题
RSA加解密
排序
JavaScript
二叉树
Golang
Map
链表
Spark Core
Spark
Typora
Shiro
Netty
SpringCloud
友情链接
申请
范明明
庄严博客
Mx
陶小桃Blog
虫洞