context包的起源
context是Go中用来进程通信的一种方式,其底层是借助channl与snyc.Mutex实现的。
context
包是在go1.7
版本中引入到标准库中的:
context
可以用来在goroutine
之间传递上下文信息,相同的context
可以传递给运行在不同goroutine
中的函数,上下文对于多个goroutine
同时使用是安全的,context
包定义了上下文类型,可以使用background
、TODO
创建一个上下文,在函数调用链之间传播context
,也可以使用WithDeadline
、WithTimeout
、WithCancel
或 WithValue
创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:context
的作用就是在不同的goroutine
之间同步请求特定的数据、取消信号以及处理请求的截止日期。
目前我们常用的一些库都是支持context
的,例如gin
、database/sql
等库都是支持context
的,这样更方便我们做并发控制了,只要在服务器入口创建一个context
上下文,不断透传下去即可。
基本介绍
context的底层设计,我们可以概括为2个接口,4种实现与6个方法。
2个接口 :Context 接口和canceler 接口
4 种实现 :emptyCtx 实现了一个空的context,可以用作根节点 cancelCtx 实现一个带cancel功能的context,可以主动取消 timerCtx 实现一个通过定时器timer和截止时间deadline定时取消的context valueCtx 实现一个可以通过 key、val 两个字段来存数据的context
6 个方法 :Background 返回一个emptyCtx作为根节点 TODO 返回一个emptyCtx作为未知节点 WithCancel 返回一个cancelCtx WithDeadline 返回一个timerCtx WithTimeout 返回一个timerCtx WithValue 返回一个valueCtx
结构和主要方法
主要函数、结构体和变量说明:
名称 | 类型 | 可否导出 | 说明 |
---|---|---|---|
Context | 接口 | 可以 | Context 基本接口,定义了 4 个方法 |
canceler | 接口 | 不可以 | Context 取消接口,定义了 2 个方法 |
CancelFunc | 函数 | 可以 | 取消函数签名 |
Background | 函数 | 可以 | 返回一个空的 Context,常用来作为根 Context |
Todo | 函数 | 可以 | 返回一个空的 context,常用于初期写的时候,没有合适的 context 可用 |
emptyCtx | 结构体 | 不可以 | 实现了 Context 接口,默认都是空实现,emptyCtx 是 int 类型别名 |
cancelCtx | 结构体 | 不可以 | 可以被取消 |
valueCtx | 结构体 | 不可以 | 可以存储 k-v 信息 |
timerCtx | 结构体 | 不可以 | 可被取消,也可超时取消 |
WithCancel | 函数 | 可以 | 基于父 context,创建可取消 Context |
WithDeadline | 函数 | 可以 | 创建一个有 deadline 的 context |
WithTimeout | 函数 | 可以 | 创建一个有 timeout 的 context |
WithValue | 函数 | 可以 | 创建一个存储 k-v 的 context |
newCancelCtx | 函数 | 不可以 | 创建一个可取消的 context |
propagateCancel | 函数 | 不可以 | 向下传递 context 节点间的取消关系 |
parentCancelCtx | 函数 | 不可以 | 找到最先出现的一个可取消 Context |
removeChild | 函数 | 不可以 | 将当前的 canceler 从父 Context 中的 children map 中移除 |
background | 变量 | 不可以 | 包级 Context,默认的 Context,常作为顶级 Context |
todo | 变量 | 不可以 | 包级 Context,默认的 Context 实现,也作为顶级 Context,与 background 同类型 |
closedchan | 变量 | 不可以 | channel struct{}类型,用于信息通知 |
Canceled | 变量 | 可以 | 取消 error |
DeadlineExceeded | 变量 | 可以 | 超时 error |
cancelCtxKey | 变量 | 不可以 | int 类型别名,做标记用的 |
Context接口
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
其中:
-
Deadline
方法需要返回当前Context
被取消的时间,也就是完成工作的截止时间(deadline); -
Done
方法需要返回一个Channel
,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done
方法会返回同一个Channel; -
Err
方法会返回当前Context
结束的原因,它只会在Done
返回的Channel被关闭时才会返回非空的值;如果当前
Context
被取消就会返回Canceled
错误;如果当前
Context
超时就会返回DeadlineExceeded
错误; -
Value
方法会从Context
中返回键对应的值,对于同一个上下文来说,多次调用Value
并传入相同的Key
会返回相同的结果,该方法仅用于传递跨API和进程间跟请求域的数据;
Context 的四种实现
emptyCtx
从源码可以看出,emptyCtx 实际上就是个 int,其对 Context 接口的实现不是直接返回,就是返回 nil,是一个空实现。它通常用于创建 root Context,标准库中 context.Background() 和 context.TODO() 返回的就是这个 emptyCtx。emptyCtx 不能取消、不能传值且没有 deadline。
cancelCtx
Context 包的核心实现就是 cancelCtx,包括里面的构造树形结构、级联取消等。
Dono()方法:
c.done 是“懒汉式”初始化,只有调用了 Done() 方法的时候才会被创建。Done() 方法用于通知该 Context 是否被取消,通过监听 channel 关闭达到被取消通知目的,c.done 没有被关闭的时候,调用 Done() 方法会 block,被关闭之后,调用 Done() 方法返回 struct{},一般通过搭配 select 使用。
Value()方法
这个方法的实现比较有意思,cancelCtxKey 是一个 Context 包内部变量,将 key 与 &cancelCtxKey 比较,相等的话就返回 *cancelCtx,即 cancelCtx 的自身地址;否则继续递归。
canceler 接口
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
如果一个 Context 类型实现了上面定义的两个方法,该 Context 就是一个可取消的 Context。Context 包中 *cancelCtx 和 *timerCtx 实现了 canceler 接口,注意这里是指针类型。
第一次看到这两个接口时,我就在想为什么不把 canneler 和 Context 合并呢?况且他们定义的方法中都有 Done 方法,可以解释得通的说法是,源码作者认为 cancel 方法并不是 Context 必须的,根据最小接口设计原则,将两者分开。像 emptyCtx 和 valueCtx 不是可取消的,所以他们只要实现 Context 接口即可。cancelCtx 和 timerCtx 是可取消的 Context,他们要实现 2 个接口中的所有方法。
Background()和TODO()
Go内置两个函数:Background()
和TODO()
,这两个函数分别返回一个实现了Context
接口的background
和todo
。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context
,衍生出更多的子上下文对象。
Background()
主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根Context。
TODO()
,它目前还不知道具体的使用场景,如果我们不知道该使用什么Context的时候,可以使用这个。
background
和todo
本质上都是emptyCtx
结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。
With系列函数
context
包中还定义了四个With系列函数。
WithCancel
WithCancel
返回带有新Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论先发生什么情况。
WithDeadline
返回父上下文的副本,并将deadline调整为不迟于d。如果父上下文的deadline已经早于d,则WithDeadline(parent, d)在语义上等同于父上下文。当截止日过期时,当调用返回的cancel函数时,或者当父上下文的Done通道关闭时,返回上下文的Done通道将被关闭,以最先发生的情况为准。
WithTimeout
取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel,通常用于数据库或者网络连接的超时控制。具体示例如下:
WithValue
WithValue
返回父节点的副本,其中与key关联的值为val。
仅对API和进程间传递请求域的数据使用上下文值,而不是使用它来传递可选参数给函数。
所提供的键必须是可比较的,并且不应该是string
类型或任何其他内置类型,以避免使用上下文在包之间发生冲突。WithValue
的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}
。或者,导出的上下文关键变量的静态类型应该是指针或接口。
客户端超时取消示例
调用服务端API时如何在客户端实现超时控制?
client端
package main
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"sync"
"time"
)
// 客户端
type respData struct {
resp *http.Response
err error
}
func doCall(ctx context.Context) {
transport := http.Transport{
// 请求频繁可定义全局的client对象并启用长链接
// 请求不频繁使用短链接
DisableKeepAlives: true, }
client := http.Client{
Transport: &transport,
}
respChan := make(chan *respData, 1)
req, err := http.NewRequest("GET", "http://127.0.0.1:8000/", nil)
if err != nil {
fmt.Printf("new requestg failed, err:%v\n", err)
return
}
req = req.WithContext(ctx) // 使用带超时的ctx创建一个新的client request
var wg sync.WaitGroup
wg.Add(1)
defer wg.Wait()
go func() {
resp, err := client.Do(req)
fmt.Printf("client.do resp:%v, err:%v\n", resp, err)
rd := &respData{
resp: resp,
err: err,
}
respChan <- rd
wg.Done()
}()
select {
case <-ctx.Done():
//transport.CancelRequest(req)
fmt.Println("call api timeout")
case result := <-respChan:
fmt.Println("call server api success")
if result.err != nil {
fmt.Printf("call server api failed, err:%v\n", result.err)
return
}
defer result.resp.Body.Close()
data, _ := ioutil.ReadAll(result.resp.Body)
fmt.Printf("resp:%v\n", string(data))
}
}
func main() {
// 定义一个100毫秒的超时
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100)
defer cancel() // 调用cancel释放子goroutine资源
doCall(ctx)
}
server端
package main
import (
"fmt"
"math/rand"
"net/http"
"time"
)
// server端,随机出现慢响应
func indexHandler(w http.ResponseWriter, r *http.Request) {
number := rand.Intn(2)
if number == 0 {
time.Sleep(time.Second * 10) // 耗时10秒的慢响应
fmt.Fprintf(w, "slow response")
return
}
fmt.Fprint(w, "quick response")
}
func main() {
http.HandleFunc("/", indexHandler)
err := http.ListenAndServe(":8000", nil)
if err != nil {
panic(err)
}
}
总结及注意事项
context 包的代码非常短,去掉注释的话也就 200+ 行,但却是并发控制的标准做法,比如实现 goroutine 之间传递取消信号、截止时间及传递一些 k-v 值等。
注意事项
- 以Context作为参数的函数方法,应该把Context作为第一个参数。
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
- Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
- 推荐以参数的方式显示传递Context
- 对第三方调用要传入 context,用于控制远程调用。
- 不要将上下文存储在结构类型中,尽可能的作为函数第一位形参传入。
- 函数调用链必须传播上下文,实现完整链路上的控制。
- context 的继承和派生,保证父、子级 context 的联动。
- 不传递 nil context,不确定的 context 应当使用 TODO。
- context 仅传递必要的值,不要让可选参数揉在一起。
优点
- 使用
context
可以更好的做并发控制,能更好的管理goroutine
滥用。 context
的携带者功能没有任何限制,这样我我们传递任何的数据,可以说这是一把双刃剑- 网上都说
context
包解决了goroutine
的cancelation
问题,你觉得呢?
缺点
context
可以携带值,但是没有任何限制,类型和大小都没有限制,也就是没有任何约束,这样很容易导致滥用,程序的健壮很难保证;还有一个问题就是通过context
携带值不如显式传值舒服,可读性变差了。- 可以自定义
context
,这样风险不可控,更加会导致滥用。 - 创建衍生节点实际是创建一个个链表节点,其时间复杂度为O(n),节点多了会掉支效率变低。
评论区