[TOC]
## Context
对服务器传入的请求应该创建上下文,而对服务器的传出调用应该接受上下文。它们之间的函数调用链必须传递上下文,或者可以使用`WithCancel`、`WithDeadline`、`WithTimeout`或`WithValue`创建的派生上下文。当一个上下文被取消时,它派生的所有上下文也被取消。
### 阅读延申
https://mp.weixin.qq.com/s/GldXbE9z-2FkWkEs_eE1pg
### Context接口构成
~~~
type Context interface {
// 返回 context 是否设置了超时时间以及超时的时间点
// 如果没有设置超时,那么 ok 的值返回 false
// 每次调用都会返回相同的结果
Deadline() (deadline time.Time, ok bool)
// 如果 context 被取消,这里会返回一个被关闭的 channel
// 如果是一个不会被取消的 context,那么这里会返回 nil
// 每次调用都会返回相同的结果
Done() <-chan struct{}
// 返回 done() 的原因
// 如果 Done() 对应的通道还没有关闭,这里返回 nil
// 如果通道关闭了,这里会返回一个非 nil 的值:
// - 若果是被取消掉的,那么这里返回 Canceled 错误
// - 如果是超时了,那么这里返回 DeadlineExceeded 错误
// 一旦被赋予了一个非 nil 的值之后,每次调用都会返回相同的结果
Err() error
// 获取 context 中保存的 key 对应的 value,如果不存在则返回 nil
// 每次调用都会返回相同的结果
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和进程间跟请求域的数据;
**其主要的应用 :**
1:上下文控制,
2:多个 goroutine 之间的数据交互等,
3:超时控制:到某个时间点超时,过多久超时。
### Context 类型的结构体
#### emptyCtx
emptyCtx 不是一个结构体,它只是 int 类型的一个别名,实现的 Context 的四个方法都是返回 nil 或者默认值:
~~~
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
~~~
这意味着 emptyCtx 永远不能被取消,没有 deadline,并且也不会保存任何值。它是一个私有类型,没有提供相关的导出方法,但是却被包装成了两个可以被导出的 ctx,用作顶层 Context:
~~~
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
~~~
### Background()和TODO()
* background 通常可以用于 main 函数、初始化和测试,作为请求上下文的最顶层(根节点)。
* todo 当你不知道需要传入什么样的 context 的时候,就可以使用它,它可以随时被替换成其他类型的 context。
实际上这俩完全没有任何区别,但是通过不同的命名
`background`和`todo`本质上都是`emptyCtx`结构体类型,是一个不可取消,没有设置截止时间,没有携带任何值的Context。
~~~
创建父节点
context.Background()
context.TODO()
~~~
### With函数
#### WithCancel
~~~go
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
~~~
返回:父节点的副本,cancle的函数
#### WithDeadline
~~~go
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
~~~
入参:deadline 的截至时间点,代表到了这个时间就会自动取消。
eg:
~~~
ctx, _ := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
fmt.Println(ctx.Deadline())
//return:2022-07-18 11:39:07.3568866 +0800 CST m=+60.002051901 true
~~~
#### WithTimeout
~~~go
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
~~~
入参:持续时间,代表 ctx 会在多长时间之后自动取消 eg:入参是5s,那么就5秒结束
#### WithValue
~~~go
func WithValue(parent Context, key, val interface{}) Context
~~~
入参:父节点副本,key,val,设置值使用
eg:
~~~
type MySting string //定义类型
func main() {
var s MySting
var s1 MySting
s = "log"
s1 = "log1"
ctx := context.WithValue(context.Background(), s, 99)
fmt.Println(ctx.Value(s))
ctx1 := context.WithValue(ctx, s1, 100)
fmt.Println(ctx1 .Value(s1))
}
// return : 99 100
~~~
> 一个 ctx 只能保存一对 kv,那么如果我们想要在 ctx 中保存多个 kv 键值对该怎么办?
只需要多次调用 `WithValue()` 函数,每次塞进去一对 kv 即可,在取值的时候,`Value()` 方法会自动地从整个 ctx 的树形结构中递归地往上层查找。所以,我们可以通过子 ctx 找到 父 ctx 维护的 kv,但是反过来是不可以的,这一点在使用的过程中需要注意。
举个例子,如下图所示,假设当前在 Context3,我们想要查 key1 的值,发现当前 ctx 维护的 key 不是 key1,那么会从它的父节点也就是 Context2 去找,还没找到,便继续往上层去找,发现 Context1 维护的 key 就是我们想要的,那么 `Value` 方法便会返回它对应的 val1。
![](https://img.kancloud.cn/c3/96/c3969901bce79c032e0576d5a48b839f_1080x191.png)
## context 使用中的注意事项
1、context 携带的 kv 是向上查找的,如果当前节点查不到对应的 key,那么会继续从其父节点中查找;
2、context 的取消操作是向下蔓延的,如果当前节点取消,那么它的子节点(cancelCtx)也会被取消;
3、使用带有超时的 timerCtx,如果能提前取消,那么最好手动提前取消,从而可以快速释放资源,同时需要注意的是 context 的取消操作针对的只是 context,如果还涉及到一些其他的操作,例如和数据库通信、文件读写等,这些也需要我们手动取消。
以下几点是使用 context 的一些约定俗成的建议:
1、不要将 context 塞到结构体里面,相反的它应该作为函数的第一个参数,并且统一命名成 ctx;
2、不要传入一个 nil context,如果不知道传啥,可以使用 context.TODO() 传入一个 emptyCtx;
3、context 中存储的应该是贯穿整个生命周期的数据,例如用户的 session、cookie 等,不要把本应该作为函数参数的数据放进 context 中;
4、**key 的类型最好不要是字符串类型或者其它内建类型**,否则容易在包之间使用 Context 时候产生冲突。使用 `WithValue` 时,**key 的类型最好是自己定义的类型**;
5、**context 是天然并发安全的**,不需要担心多个 goroutine 对它的并发操作。
### 典型应用
#### 数据传递
~~~
const KEY_LOG = "LOG_ID"
const KEY_USER_ID = "USER_ID"
func TestWithValue(t *testing.T) {
// 通过 WithValue() 生成一个保存 key-value 键值对的 ctx
ctx := context.WithValue(context.Background(), KEY_LOG, "2021082900001")
// 链式存入第二个 key
ctx = context.WithValue(ctx, KEY_USER_ID, "112233")
logId := GetLogID(ctx)
t.Log(logId)
}
func GetLogID(ctx context.Context) string {
// 通过 Value() 方法查找
if logId := ctx.Value(KEY_LOG); logId != nil {
return logId.(string)
}
return ""
}
~~~
#### 取消协程执行
通过检查 Context 的 `Done` 方法我们可以判断它是否被 cancel 了。同时 context 还提供了两个带有超时功能的方法,分别是 `WithTimeout` 和 `WithDeadline` ,它们本质上是一样的,只不过前者的入参是超时时间,后者的入参是截至的时间,通过这两个方法生成的 ctx,都能够实现在时间到了之后,自动执行 `cancel` 方法,当然我们也可以选择(最好)在超时时间到来之前手动调用 `cancel` 。在很多微服务调用的实现场景,都是通过它们来实现远程调用的超时控制的。
~~~
func TestWithCancel(t *testing.T) {
ctx, cancel := context.WithCancel(ctx)
// WithTimeout 可以实现超时自动调用 Cancel()
// ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
go TakeTasks(ctx, "c1")
// ctx2 是 ctx 的子 context,当 ctx 被取消之后,ctx2 也会被取消
ctx2, _ := context.WithCancel(ctx)
go TakeTasks(ctx2, "c2")
time.Sleep(500 * time.Millisecond)
// 也可以手动提前取消
cancel()
time.Sleep(100 * time.Millisecond)
}
func cancelled(ctx context.Context) bool {
select {
case <-ctx.Done():
fmt.Println("finish taking tasks!")
return true
default:
fmt.Println("continue!")
return false
}
}
func TakeTasks(ctx context.Context, flag string) {
for {
if cancelled(ctx) {
break
}
fmt.Printf("%s taking tasks!\n", flag)
time.Sleep(100 * time.Millisecond)
}
}
~~~
context 的取消操作是一层一层往下传递的。也就是说在调用 `cancel()` 之后,对应的 ctx 会先标记自己已经被取消,然后它会向它的所有子 ctx 传达取消信号,通知它们也应该被取消掉了。
- Go准备工作
- 依赖管理
- Go基础
- 1、变量和常量
- 2、基本数据类型
- 3、运算符
- 4、流程控制
- 5、数组
- 数组声明和初始化
- 遍历
- 数组是值类型
- 6、切片
- 定义
- slice其他内容
- 7、map
- 8、函数
- 函数基础
- 函数进阶
- 9、指针
- 10、结构体
- 类型别名和自定义类型
- 结构体
- 11、接口
- 12、反射
- 13、并发
- 14、网络编程
- 15、单元测试
- Go常用库/包
- Context
- time
- strings/strconv
- file
- http
- Go常用第三方包
- Go优化
- Go问题排查
- Go框架
- 基础知识点的思考
- 面试题
- 八股文
- 操作系统
- 整理一份资料
- interface
- array
- slice
- map
- MUTEX
- RWMUTEX
- Channel
- waitGroup
- context
- reflect
- gc
- GMP和CSP
- Select
- Docker
- 基本命令
- dockerfile
- docker-compose
- rpc和grpc
- consul和etcd
- ETCD
- consul
- gin
- 一些小点
- 树
- K8s
- ES
- pprof
- mycat
- nginx
- 整理后的面试题
- 基础
- Map
- Chan
- GC
- GMP
- 并发
- 内存
- 算法
- docker