一: Context 基本介绍
Go 1.7 标准库引入context
,译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。
随着 context 包的引入,标准库中很多接口因此加上了 context 参数,例如database/sql
包。context 几乎成为了并发控制和超时控制的标准做法
作用:
在一组 goroutine 之间传递共享的值、取消信号、deadline
在 HTTPServer 中 :
Context 2 中 , 三个 goroutine 传递的是相同的请求
二: Context 核心结构
context.Context
是 Go 语言在 1.7 版本中引入标准库的接口,该接口定义了四个需要实现的方法:
type Context interface {
// 返回被取消的时间
Deadline() (deadline time.Time, ok bool)
// 返回用于通知Context完结的channel
// 当这个 channel 被关闭时,说明 context 被取消了
// 在子协程里读这个 channel,除非被关闭,否则读不出来任何东西
Done() <-chan struct{}
// 返回Context取消的错误
Err() error
// 返回key对应的value
Value(key any) any
}
Done()
: 会关闭传递信号的 channel , 当 channel 被关闭了 , 就可以接收到数据了 ( context 被取消了 )
value()
: 通过key:value
来传递context中需要共享的值
除了Context接口,还存在一个 canceler 接口,用于实现 Context 可以被取消
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
可以取消的上下文都应该实现 canceler 接口
除了以上两个接口,还有4个预定义的Context类型:
// 空Context
type emptyCtx int
// 取消Context
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value // of chan struct{}, created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
// 定时取消Context
type timerCtx struct {
cancelCtx
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
// KV值Context
type valueCtx struct {
Context
key, val any
}
三: 默认 Context 使用
context 包中最常用的方法是context.Background
、context.TODO
,这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用:
context.Background
, 是上下文的默认值,所有其他的上下文都应该从它衍生出来,在多数情况下,如果当前函数没有上下文作为入参,我们都会使用context.Background
作为起始的上下文向下传递。context.TODO
,是一个备用,一个 context 占位,通常用在并不知道传递什么 context的情形。
底层都是返回一个空
context
// 创建方法
func Background() Context {
return background
}
func TODO() Context {
return todo
}
// 预定义变量
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// emptyCtx 定义
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 any) any {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
作用:
有些函数或者方法需要 context , 但是自己没有 context , 这时候就可以定义空 context
database/sql
包中的某些函数
func (db *DB) PingContext(ctx context.Context) error
func (db *DB) ExecContext(ctx context.Context, query string, args ...any) (Result, error)
func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error)
func (db *DB) QueryRowContext(ctx context.Context, query string, args ...any) *Row
具体使用
db, _ := sql.Open("", "")
query := "DELETE FROM `table_name` WHERE `id` = ?"
db.ExecContext(context.Background(), query, 42)
四: Contex 主动传递取消信号
1. 主动取消
需要的操作为:
创建带有cancel函数的Context,
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
接收 cancel 的Channel,
ctx.Done()
主动 Cancel 的函数,
cancel CancelFunc
具有同一个 context 对象的 goroutine , 调用主动 Cancel 取消函数后 , ctx.Done()
就可以取出数据
每一个 goroutine 都会监听c.Done()
这个 channel , 当监听到之后就会执行用户自己定义的操作 , 比如return 退出所有goroutine
取消这个操作不是由 context 执行的 , context 只负责去传递这个信号 , 有了这个信号 , 就可以基于这个信号做后续操作
func ContextCancel() {
// 一:创建带有cancel函数的context
ctx, cancel := context.WithCancel(context.Background())
// 二: 启动goroutine,携带cancelCtx
wg := sync.WaitGroup{}
wg.Add(4)
for i := 0; i < 4; i++ {
go func(c context.Context, n int) {
defer wg.Done()
// 监听context的取消完成channel,来确定是否执行主动cancel操作
fmt.Println("第", n, "个Goroutine")
for {
select {
// 等待接收c.Done()这个channel
case <-c.Done():
fmt.Println("第", n, "个 Goroutine ", "context cancel")
return
default:
}
time.Sleep(300 * time.Millisecond)
}
}(ctx, i)
}
// 三: 定时取消cancel()
// 定时器,三秒后取消所有goroutine的执行
select {
case <-time.NewTimer(3 * time.Second).C:
fmt.Println("3秒时间到")
cancel()
}
// 也可以使用select解决goroutine结束无法打印contex cancel问题
select {
case <-ctx.Done():
fmt.Println("main context cancel")
}
wg.Wait()
}
2. Deadline 和 Timeout 定时取消
context.WithTimeout()
某个时间段 , 比如5秒后context.WithDeadline()
某个时间点 . 比如每天20:30
// 10s后cancel
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
// 20:30 cancel
// time.Data()的方式
curr := time.Now()
t := time.Date(curr.Year(), curr.Month(), curr.Day(), 20, 30, 0, 0, time.Local)
ctx, cancel := context.WithDeadline(context.Background(), t)
// 以当前的时间加10分钟
context.WithDeadline(context.Background(),time.Now().Add( 10 * time.Minute))
带有时间的自动取消也可以自行调用Cancel()
来实现主动取消
select {
// 3秒后主动取消
case <-time.NewTimer(3 * time.Second).C:
cancel()
// 通过withTimeout自动取消
// context.WithTimeout(context.Background(), 10 * time.Second)
case ctx.Done()
}
使用场景 : 不能确定主动调用是否能够调用成功 , 就可以使用但是取消
从底层开始看出 , 定时取消是在主动取消的基础上增加的功能
type timerCtx struct {
cancelCtx // 主动取消
timer *time.Timer // Under cancelCtx.mu.
deadline time.Time
}
而
WithTimeout()
也是利用WithDeadline()
实现的
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
3. Cancel 操作的向下传递
当父上下文被取消时 , 子上下文也会被取消
func ContextExtends() {
// 定义符合上图的context结构
ContextOne, _ := context.WithCancel(context.Background())
ContextTwo, cancel := context.WithCancel(ContextOne)
ContextThree, _ := context.WithCancel(ContextOne)
ContextFour, _ := context.WithCancel(ContextTwo)
wg := sync.WaitGroup{}
wg.Add(4)
// 开启四个goroutine分别监控四个context的Done()方法
go func(c context.Context) {
defer wg.Done()
select {
case <-ContextOne.Done():
fmt.Println("contextOne cancel")
}
}(ContextOne)
go func(c context.Context) {
defer wg.Done()
select {
case <-ContextTwo.Done():
fmt.Println("contextTwo cancel")
}
}(ContextTwo)
go func(c context.Context) {
defer wg.Done()
select {
case <-ContextThree.Done():
fmt.Println("contextThree cancel")
}
}(ContextThree)
go func(c context.Context) {
defer wg.Done()
select {
case <-ContextFour.Done():
fmt.Println("contextFour cancel")
}
}(ContextFour)
// 手动取消信号
cancel()
wg.Wait()
}
通过给对应 Context 执行 cancel , 输出遵循: 当父上下文被取消时 , 子上下文也会被取消
五: 取消操作流程
1. 创建 cancelCtx 的流程
2. ctx.Done() 初始信号 channel 流程
3. cancel() 操作流程
六: Context 传值
如果在 ContextB 中设置了一个值 , 那么这个值只会在 contextA 和基于A产生的 contextC 和 contextD 中使用
Web 开发中 , 每一个新请求创建一个 context 中 , 将数据存储到 context 中 , 当前 context 中的后续调用都可以用到该值
context 数据类型 : key - value 数据
func WithValue(parent Context, key, val any) Context
type Context interface {
Value(key any) any
}
需要三个参数:
上级 Context
key 要求是 comparable 的(可比较的),实操时,推荐使用特定的 Key 类型,避免直接使用 string 或其他内置类型而带来 package 之间的冲突。
// 避免冲突
// 例如其他包中有相同的字段"name"
type MyContextKey String
val any
1. 单个 context 传值
type MyContext string
func ContextValue() {
wg := sync.WaitGroup{}
// 1.创建带有value的context
ctx := context.WithValue(context.Background(), MyContextKey("name"), "Sakura")
// 2.将ctx传入goroutine中
wg.Add(1)
key := "name"
go func(c context.Context, key any) {
defer wg.Done()
// 通过key拿到value
if value := c.Value(key); value != nil {
fmt.Println("value:", value)
return
}
fmt.Println("找不到key为\"", key, "\"的数据")
}(ctx, key)
wg.Wait()
}
要注意获取到的 value 为 any 空接口类型 , 如果想要使用 value 需要进行断言或者类型判断
查看
WithValue()
可以返回返回值为valueCTX
func WithValue(parent Context, key, val any) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
type valueCtx struct {
Context
key, val any
}
// valueCtx实现了这个value这个方法
func (c *valueCtx) Value(key any) any {
if c.key == key {
return c.val
}
return value(c.Context, key)
}
也就是除了 value 功能,其他 Contenxt 功能都由parent Context实现。
如果
context.valueCtx.Value
方法查询的 key 不存在于当前 valueCtx 中,就会从父上下文中查找该键对应的值直到某个父上下文中返回
2. 多个context传值
type MyContext string
func ContextValue() {
wg := sync.WaitGroup{}
// 1.创建带有value的context
ctxOne := context.WithValue(context.Background(), MyContext("name"), "Sakura1")
ctxTwo := context.WithValue(ctxOne, MyContext("name"), "Sakura2")
ctxThree := context.WithValue(ctxTwo, MyContext("name"), "Sakura3")
// 2.将ctx传入goroutine中
wg.Add(1)
key := "name"
go func(c context.Context, key any) {
defer wg.Done()
// 通过key拿到value
if value := c.Value(key); value != nil {
fmt.Println("value:", value)
return
}
fmt.Println("找不到key为\"", key, "\"的数据")
}(ctxThree, key)
wg.Wait()
}
不存在会返回该 context 中上一级的 value
七 : 使用 context 的注意事项
推荐以参数的方式显示传递Context
以Context作为参数的函数方法,应该把Context作为第一个参数。
给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO()
Context的Value相关方法应该传递请求域的必要数据,不应该用于传递可选参数
Context是线程安全的,可以放心的在多个goroutine中传递
评论区