背景
我滴乖,看到这个名字时不禁暗叹一声,这又是一个什么神仙操作,没见过更没有听说过,还是专业面太窄了,还是开启学习之旅,老规矩还是WWH来说下我的认识,欢迎指正.
What
什么是快乐星球?什么是快乐星球?哈哈有点跑题了
context是什么? Go 1.7标准库引入context, 中文译作“上下文”,准确说它是goroutine的上下文。主要用来在goroutine之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v等。
随着context 包的引入,标准库中很多接口因此加上了 context 参数,例如database/sql包等。使用context几乎成为并发控制和超时控制的标准做法,与它协作的API 都可以由外部控制执行“取消”操作,例如:取消一个HTTP请求的执行。另外,cnextContet可以协调多个goroutine 中的代码执行“取消"操作,并且可以存储健值对,最重要的是它是并发安全的操作。
Why
在官方博客里,对于使用context 提出了几点建议:
1)不要将context塞到结构体里。直接将context类型作为函数的第一个参数, 而且般都命名为ctx.
2)不要向函数传入一个含有nil属性的context,如果实在不知道传什么,标准库准备好了一个 context: todo.
3)不要把本应该作为函数参数的类型塞到context中,context存储的应该是一些共同的数据。例如:登录的session cookie 等。
4)同一个context可能会被传递到多个goruntine但别担心,context 是并发安全的。
Go语言中的server 实际上是一个“协程模型”,处理一个请求需要多个协程。 例如在业务的高峰期,某个下游服务的响应速度变慢,而当前系统的请求又没有超时控制,或者超时时间设置过大,那么等待下游服务返回数据的协程就会越来越多。而协程是要消耗系统资源的,后果就是协程数激增,内存占用飙涨,Go调度器和GC不堪其重,甚至导致服务不可用。更严重的会导致雪崩效应,整个服务对外不可用,这肯定是高级别的事故
其实前面描述的事故,通过设置“允许下游最长处理时间”就可以避免。例如,给下 游设置的timeout 是50ms, 如果超过这个值还没有接收到返回数据,就直接向客户端返回一个默 认值或者错误。例如返回商品的一- 个默认库存数量。注意,这里设置的超时时间和创建一个HTTP client设置的读写超时时间不一一样,后者表示一次TCP传输的时间,而次请求可能包含 多次TCP传输,前者则表示所有传输的总时间。
而context包就是为了解决上面所说的这些问题而开发的:在一组goroutine 之间传递共享的 值、取消信号、deadline 等
在Go里,不能直接杀死协程,协程的关闭般采用channel和select 的方式来控制。但是在某些场景下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享些全局变量、有共同 deadline等,而且可以同时被关闭。用channel和selet就会比较麻烦,这时可以通过context 来实现。 一句话: context 用来解决goroutine 之间退出通知、元数据传递的功能的问题。
How
上面说了那么多东西那么我们怎么来使用context呢?只需要在函数中传递context就行,看下demo
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
process(ctx)
ctx = context.WithValue(ctx, "traceId", "测试上下文")
process(ctx)
}
func process(ctx context.Context) {
traceid, ok := ctx.Value("traceId").(string)
if ok {
fmt.Printf("获取到的值: %s\n", traceid)
} else {
fmt.Println("此时无值")
}
}
运行后的结果为:
此时无值
获取到的值: 测试上下文
第一.次调用process 函数时,ctx 是一个空的context, 自然取不出来traceld. 第二次,通过 WithValue函数创建了-一个context,并赋上了traceld这个key,自然就能取出来传入的value 值。
另外一个context的应用场景就是定时取消了,例如:我们去请求一个接口获取数据的数据,这需要花费一定的时间, 若碰到网络延迟、机器负载过高等问题时,会导致获得的数据时间过长,导致请求阻塞,严重地会引起雪崩。这时候就可以用context的定时取消功能:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel() //避免其他地方忘记cancel, 且重复调用不影响
ids := fetchApiData(ctx)
fmt.Println(ids)
}
func fetchApiData(ctx context.Context) (res []int64) {
select {
case <-time.After(3 * time.Second):
return []int64{100, 200, 300}
case <-ctx.Done():
return []int64{11, 22, 33}
}
}
在main函数里,首先创建了一个定时1s 的context, 到时间后会自动调用cancel 函数,接着调用fetchApiData函数获取网络数据,最后打印返回的ids.
在fetchApiData函数里,则通过设置3s的定时器,表示处理的时长,正常会返回[100 200 300],若context被取消,则返回默认值[11,22,33]。
运行程序,结果如下:
[11,22,33]
若将main函数里的context超时时间改成5s, 则最终打印:
[100 200 300]
注意一个细节,WithTimeOut 函数返回的context 和cancelFun 是分开的,context 本身并没有取消函数,这样做的原因是取消函数只能由外层函数调用,防止子节点context 调用取消函数, 从而严格控制信息的流向:由父节点context 流向子节点context。
再有就是context应用在防止goroutine泄漏,上面的demo中如果不加context,goroutine最终还是会自己执行完,最后返回,但某些场景下,如果不用context取消,goroutine就会泄漏,来看个demo:
package main
import (
"fmt"
"time"
)
//gorountine泄漏
func gen() <-chan int {
ch := make(chan int)
go func() {
var n int
for {
ch <- n
n++
time.Sleep(time.*Second)
}
}()
return ch
}
func main() {
for n := range gen() {
fmt.Println("nnnnn", n)
if n == 5 {
break
}
}
}
主函数main中调用一个可以生成无限个整数的函数gen(),当主函数中判断n==5时,直接break掉,但是gen()函数中的协程依然会执行下去永不停止,也就发生了泄漏
那么改进后的demo增加了一个context,在break之前调用了cacel函数,取消goroutine,gen()在接收到取消信号后直接退出了,系统回收资源
package main
import (
"context"
"fmt"
"time"
)
//防止gorountine泄漏
func gen(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
var n int
for {
select {
case <-ctx.Done():
return
case ch <- n:
n++
time.Sleep(time.*Second)
}
}
}()
return ch
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
for n := range gen(ctx) {
fmt.Println("nnnnn", n)
if n == 5 {
cancel()
break
}
}
}
源码
context包连带注释一起共500多行,实现的一些具体的工功能如下所示
| Context | 接口 | 定义了Context接口的四个方法 |
|---|---|---|
| emptyCtx | 结构体 | 实现了Context接口,它其实是空的Context |
| CancelFunc | 函数 | 取消函数 |
| Canceler | 接口 | Context取消接口,定义了两个方法 |
| timerCtx | 结构体 | 超时会被取消 |
| valueCtx | 结构体 | 可以存储k-v对 |
| Background | 函数 | 返回一个空的Context,常作为根Context |
| TODO | 函数 | 返回一个空Context,常用于重构时期,没有合适的Context可用 |
| WithCancel | 函数 | 基于父Context,生成一个可取消的Context |
| newCancelCtx | 函数 | 创建一个可取消的Context |
| propagateCacel | 函数 | 向下传递Context结点间的取消关系 |
| parmCancelCtx | 函数 | 找到第一个可取消的父结点 |
| removeChild | 函数 | 去掉父节点的子结点 |
| init | 函数 | 包初始化 |
| WithDeadline | 函数 | 创建一个有deadline的Context |
| WithTimeout | 函数 | 创建一个有timeout的Context |
| WithValue | 函数 | 创建一个存储k-v对的Context |
其中实现的代码逻辑有兴趣的同学可以去Context.go文件里去具体看下实现的逻辑
最后
大哥说如果有可能的话把Context作为你的函数的第一个参数传递以后能省很多事