context
简介
在 Go 语言中,context 可以用于在 goroutine 之间传递取消信号、设置截止时间以及携带请求范围的键值对数据的一种机制。
context 包提供了管理这些上下文对象的 API,使得服务端可以更好地处理诸如 HTTP 请求之类的长时间运行的任务,尤其是在涉及到并发处理的情况下。
Context 的基本用法
ctx := context.Background()
context.Background() 函数返回一个空的 Context 对象,通常用于在主函数中创建 Context 对象。
内置取消原因
内置的取消原因有:
context.Canceled
取消context.DeadlineExceeded
超时
获取取消原因
context.Cause
触发取消时,返回预设的取消原因,若无则同ctx.Err(),error类型ctx.Err()
返回取消原因,为context中预设,可作为错误内容判断的依据,error类型
WithValue 携带键值对数据
上下文中携带键值对数据的方式,通过 context.WithValue() 函数实现。
context.WithValue()
初始化一个携带键值对数据的 Context 对象。ctx.Value()
获取 Context 对象中的键值对数据。
// 上下文,携带内容
type Key string
func TestContextWithValue(t *testing.T) {
kvs := map[string]string{
"title": "Fan的小破站",
"domain": "fanfine.cn",
"name": "fan",
}
// 这里使用自定义类型是因为 key 的位置在使用内置类型时会出现提示:
// should not use built-in type string as key for value; define your own type to avoid collisions (SA1029) go-staticcheck
var key Key = "key"
ctx := context.WithValue(context.Background(), key, kvs)
fmt.Println(ctx.Value(key)) // map[domain:fanfine.cn name:fan title:Fan的小破站]
kvs["other"] = "更多信息"
fmt.Println(ctx.Value(key)) // map[domain:fanfine.cn name:fan other:更多信息 title:Fan的小破站]
}
WithValue的 value 可以是任何类型,包括指针、结构体、切片等。
可以使用引用类型,可以再创建 ctx 后续修改 value
模拟工作
func work(ctx context.Context) {
for {
select {
case v, ok := <-ctx.Done():
fmt.Println("结束时间", time.Now())
fmt.Println("结束啦!Done!", "v:", v, "ok:", ok)
return
default:
time.Sleep(time.Millisecond)
fmt.Println("正在工作")
}
}
}
ctx.Done() 返回一个只读 chan, 仅读时会保持阻塞,
所以循环中会执行默认分支,模拟工作,直到接收到 chan 被关闭的信号
当没有取消信号时,ctx.Done() 会一直阻塞,直到被取消。
当 chan 关闭时,解除了阻塞,其可以读出 ok 为false
cancel() 取消会关闭 chan
Timeout Deadline 超时会关闭 chan
WithCancel 携带取消信号
context.WithCancel()
返回一个带有取消信号的 Context 对象。ctx.Done()
获取取消信号的 channel,当取消信号发送时,channel 会被关闭。cancel()
发送取消信号,关闭 channel。
func TestContextCancel(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
go work(ctx)
time.Sleep(time.Second * 2)
cancel()
fmt.Println(context.Cause(ctx)) // context canceled
fmt.Println(ctx.Err()) // context canceled
if ctx.Err() == context.Canceled {
fmt.Println("确认是取消了")
}
}
如果不使用 cancel(),<- ctx.Done() 会一直阻塞,work() 不会结束。
那么 cancel() 后会发生什么呢?见下文
cancel 源码
// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
// cancel sets c.cause to cause if this is the first time c is canceled.
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
if err == nil {
panic("context: internal error: missing cancel error")
}
if cause == nil {
cause = err
}
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
c.cause = cause
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d) // 关闭通道
}
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err, cause)
}
c.children = nil
c.mu.Unlock()
if removeFromParent {
removeChild(c.Context, c)
}
}
cancel()函数首先判断是否已经取消,如果已经取消,则直接返回。- 如果没有取消,则设置取消原因,并关闭通道。
- 遍历子上下文,递归调用
cancel()函数,关闭子上下文。 - 释放资源。
所以 cancel() 后,这个上下文就没用了,如果后续还有使用的话,需要重新创建一个。
WithCancelCause 携带取消原因
返回一个携带预设取消原因的 Context 对象。
取消时可以传入错误原因。
// 携带取消原因的 context
func TestContextCancelCause(t *testing.T) {
ctx, cancel := context.WithCancelCause(context.Background())
go work(ctx)
time.Sleep(time.Second * 2)
cancel(errors.New("2秒已经过了,该结束了")) // 取消时设置原因
fmt.Println(context.Cause(ctx)) // 为预设的错误:2秒已经过了,该结束了
fmt.Println(ctx.Err()) // context canceled
}
WithTimeout 携带超时信号
上下文携带超时时间,达到超时时间,就会自动取消
context.WithTimeout()
创建一个带有超时时间的 context
func TestContextWithTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
go work(ctx)
time.Sleep(time.Second * 2) // 等待2秒
fmt.Println("记录超时原因:方式1:", context.Cause(ctx)) // context deadline exceeded
fmt.Println("记录超时原因:方式2:", ctx.Err()) // context deadline exceeded
}
- 超时时间设置为 1 秒
- go work() 后睡眠2秒,制造超时条件
如此,会先超时,两种方法得到错误原因都是 上下文超过截止时间 context deadline exceeded
补充
context.WithTimeout() 函数内部调用了 context.WithDeadline()
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithTimeoutCause 携带超时原因
context.WithTimeoutCause()
创建一个携带超时原因的 context
// 携带超时原因的超时
func TestContextWithTimeoutCause(t *testing.T) {
ctx, cancel := context.WithTimeoutCause(context.Background(), time.Second, errors.New("执行任务超时啦"))
defer cancel()
go work(ctx)
time.Sleep(time.Second * 2) // 等待2秒
fmt.Println("记录超时原因:方式1:", context.Cause(ctx)) // 执行任务超时啦
fmt.Println("记录超时原因:方式2:", ctx.Err()) // context deadline exceeded
}
使用 context.Cause() 可以获取到预设的超时原因
而 ctx.Err() 则是 context 的预设超时原因,为 context deadline exceeded
补充
context.WithTimeoutCause() 函数内部调用了 context.WithDeadlineCause()
func WithTimeoutCause(parent Context, timeout time.Duration, cause error) (Context, CancelFunc) {
return WithDeadlineCause(parent, time.Now().Add(timeout), cause)
}
WithDeadline 携带截止时间
context.WithDeadline()
创建一个携带截止时间的 context
func TestContextDeadline(t *testing.T) {
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(1*time.Second))
defer cancel()
go work(ctx)
time.Sleep(2 * time.Second)
fmt.Println("记录超时原因:方式1:", context.Cause(ctx)) // context deadline exceeded
fmt.Println("记录超时原因:方式2:", ctx.Err()) // context deadline exceeded
}
context.Cause() ctx.Err() 都是 context deadline exceeded
WithDeadlineCause 携带截止时间原因
context.WithDeadlineCause()
创建一个有预设原因的,有截止时间的 context
func TestContextDeadlineCause(t *testing.T) {
ctx, cancel := context.WithDeadlineCause(context.Background(), time.Now().Add(1*time.Second), errors.New("超过截止时间"))
defer cancel()
go work(ctx)
time.Sleep(2 * time.Second)
fmt.Println("记录超时原因:方式1:", context.Cause(ctx)) // 超过截止时间
fmt.Println("记录超时原因:方式2:", ctx.Err()) // context deadline exceeded
}
使用 context.Cause() 可以获取到预设的超时原因
而 ctx.Err() 则是 context 的预设超时原因,为 context deadline exceeded
复合上下文
上述中 context 的初始化的方式,都是通过 context.Background() 来初始化的,
但是,也可以继承其他上下文。
只需在初始化新上的 context 时,传入一个父上下文即可。
比如:一个有超时时间(截止时间),且携带内容的上下文
// 上下文 - 继承其他上下文
func TestContextInherit(t *testing.T) {
// 携带内容的上下文 ctx
ctx := context.WithValue(context.Background(), "domain", "fanfine.cn")
// ctx 继承自 ctx
ctx, _ = context.WithTimeout(ctx, time.Second)
go work(ctx)
time.Sleep(time.Second * 2)
fmt.Println("超时之后 从 ctx 中取出 domain 值:", ctx.Value("domain"))
// 因为是紧随着上面超时的之后进行,其 ctx.Done() chan 已经关闭,
// chan 关闭后不能重开,所以只能重新创建一个 ctx
// ctx, _ = context.WithDeadline(ctx, time.Now().Add(time.Second*2))
ctx = context.WithValue(context.Background(), "domain", "fanfine.cn")
ctx, _ = context.WithDeadline(ctx, time.Now().Add(time.Second*2))
go work(ctx)
time.Sleep(time.Second * 4)
fmt.Println("截止时间后 从 ctx 中取出 domain 值:", ctx.Value("domain"))
/* 输出:
(1063)正在工作
结束时间 2024-09-18 05:24:39.392096 +0800 CST m=+1.001402793
结束啦!Done! v: {} ok: false
超时之后 从 ctx 中取出 domain 值: fanfine.cn
(1063)正在工作
结束时间 2024-09-18 05:24:42.3929 +0800 CST m=+4.002252293
结束啦!Done! v: {} ok: false
截止时间后 从 ctx 中取出 domain 值: fanfine.cn
*/
}
场景应用
在接口调用时使用context,可以设置超时时间。
超过时间后,中断接口调用,并返回错误。
// 应用场景,调用接口
func TestUrl(t *testing.T) {
url := "https://example.com/api"
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second))
defer cancel()
// 初始化request
buf := new(bytes.Buffer)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, buf)
if err != nil {
panic("初始化请求失败")
}
req.Header.Set("Content-Type", "application/json")
// 手动制造超时条件
time.Sleep(2 * time.Second)
// 执行请求
client := http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("err:", err)
if ctx.Err() == context.DeadlineExceeded {
fmt.Println("请求超时,是的")
} else {
fmt.Println("请求失败:", err)
}
return
}
defer resp.Body.Close()
// 读响应体
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
fmt.Println("读取响应体失败:", err)
return
}
fmt.Println("响应内容:", string(body))
}


