文章目录
GoLang之Context基础篇
注:本文基于Windos系统上Go SDK v1.18进行讲解
“一个接口,四种方法,六个函数”
1.一个接口
golang的context包定义了Context类型,根据官方文档的说法,该类型被设计用来在API边界之间以及过程之间传递截止时间、取消信号及其他与请求相关的数据。Context实际上是一个接口,提供了4个方法:
1.Deadline用来返回ctx的截止时间,ok为false表示没有设置。达到截止时间的ctx会被自动Cancel掉;
2.如果当前ctx是可取消的,Done返回一个chan用来监听,否则返回nil。当ctx被Cancel时,返回的chan会同时被close掉,也就变成“有信号”状态;
3.如果ctx已经被canceled,Err会返回执行Cancel时指定的error,否则返回nil;
4.Value用来从ctx中根据指定的key提取对应的value;
//context/context.go
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
2.四种实现
golang对Context接口的具体实现基于如下几种类型:
1.emptyCtx类型只是实现了Context接口,几个方法都只是简单的返回nil、false等,实际上什么也不做。Background和TODO返回的Context,实际上都是emptyCtx;
2.cancelCtx类型可以说是最核心的一个类型,它实现了Cancel操作和信号机制,以及Context父子关系关联,从而支持在父Context Cancel时同步Cancel所有子Context;
3.timerCtx类型用来支持Deadline、Timeout,Cancel操作依赖于内置的cancelCtx;
4.valueCtx类型用来支持key、val打包,结合内置的Context字段,实际上构造了一个单链表节点;
//context/context.go
type emptyCtx int
type cancelCtx struct {
Context
mu sync.Mutex // 保护其他字段
done chan struct{} // 延迟创建,被第一个cancel close
children map[canceler]struct{} // 被第一个cancel设为nil
err error // 被第一个cancel赋值(非nil)
}
type timerCtx struct {
cancelCtx
timer *time.Timer // 被cancelCtx.mu保护(并发保护)
deadline time.Time
}
type valueCtx struct {
Context
key, val interface{}
}
3.六个函数
3.1六个函数
为了方便我们使用Context,context包还实现了一组函数:
//context/context.go
func Background() Context
func TODO() Context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
3.2Background
//context/context.go
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
Background函数会创建一个没有Deadline、没有Value,也不能被Cancel的emptyCtx。通常在一个请求的初始化阶段用Background()创建最顶层的根Context。
ctx := context.Background()
ctx
是Context类型,也就是一个非空接口,它的结构如下图所示:
3.3TODO
//context/context.go
func TODO() Context {
return todo
}
TODO函数和Background一样,也会创建一个emptyCtx,官方文档建议在"本来应该使用外层传递的ctx,而外层却没有传递"的地方使用,就像函数名的含义一样,留下一个TODO。
3.4WithCancel
//context/context.go
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
WithCancel函数会基于传入的parent创建一个可以Cancel的ctx,与cancel函数一起返回。调用cancel函数就会将这个新的
ctx
Cancel掉,所有基于此ctx创建的子孙Context也会一并被Cancel掉。
func main() {
var wg sync.WaitGroup
ctx := context.Background()
ctx1, cancel = context.WithCancel(ctx)
wg.Add(1)
go func() {
defer wg.Done()
tick := time.NewTicker(300 * time.Millisecond)
for {
select {
case <-ctx1.Done():
fmt.Println(ctx1.Err())
return
case t := <-tick.C:
fmt.Println(t.Nanosecond())
}
}
}()
time.Sleep(time.Second)
cancel()
wg.Wait()
}
上面的示例把
Background()
获取的ctx
包装为可Cancel的ctx1
,它的结构如下图所示:(示例中拆分成ctx和ctx1单纯为了图好画-_-!)
通过
ctx1.Done()
获取一个channel,监听这个channel可以获得ctx1
的Cancel通知,还可以通过ctx.Err()
获取ctx1
被取消时写入的错误信息。
3.5WithDeadline
//context/context.go
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
WithDeadline内部会创建一个可以Cancel的ctx,并且为它设置一个超时时间,然后与一个cancel函数一并返回。如果用户代码没有主动调用cancel函数,则ctx会在超时时间到达后自动Cancel。
如果需要给任务设定一个截止时间,就可以用WithDeadline创建一个timerCtx。用wg来等待goroutine返回,若没有达到截止时间就完成了任务,
defer cancel()
也会把ctx2
取消掉。
func main() {
var wg sync.WaitGroup
ctx := context.Background()
ctx2, cancel := context.WithDeadline(ctx,time.Now().Add(time.Second))
defer cancel()
wg.Add(1)
go func() {
defer wg.Done()
tick := time.NewTicker(300 * time.Millisecond)
for {
select {
case <-ctx2.Done():
fmt.Println(ctx2.Err())
return
case t := <-tick.C:
fmt.Println(t.Nanosecond())
}
}
}()
wg.Wait()
}
上面的示例中传递给WithDeadline函数的,是一个没有截止时间且不可取消的Context。WithDeadline内部会基于
ctx
构建一个cancelCtx
,所以ctx2
的动态类型是*timerCtx
结构如下图所示:
如果WithDeadline函数接收到的是一个有截止时间的Context,那就要比较一下这两个截止时间了。例如下面再基于ctx2创建一个Context:
ctx3, cancel := context.WithDeadline(ctx2, time.Now().Add(time.Second*2))
如果ctx3要设定的截止时间早于ctx2,那么就会构造一个timerCtx,并且timerCtx.cancelCtx是基于ctx2构建的。
如果ctx3要设定的截止时间比ctx2还晚,那么就没必要给它加定时器和截止时间了。只会用WithCancel函数基于ctx2构造一个cancelCtx,所以ctx3的动态类型是*cancelCtx。
上面的ctx3是基于ctx2创建的,而ctx2又是基于ctx创建的。这样层层包装的结果就是,这些Context会形成树状结构,子节点是基于父节点的包装,每个节点都可能有零个或多个子节点。
对于可取消的Context而言,也就是实现了context.canceler接口的类型,还有一个关键点,就是它们会被注册到距离当前Context节点最近的、可取消的祖先节点中。注册位置就在cancelCtx结构体的children map中。而*cancelCtx和*timerCtx都实现了canceler接口,所以ctx3会被注册到ctx2中。
这样如果ctx2先取消,就可以根据children map这里的记录,取消自己子节点中所有可Cancel的Context。
3.6WithTimeout
//context/context.go
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithTimeout
只是WithDeadline
的一个wrapper,接受一个time.Duration
类型的参数,使用当前时间加上这个时间段来设置Deadline。
3.7WithValue
//context/context.go
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}
}
WithValue
将传入的parent
Context和key
、val
打包成一个新的ctx
,再借助valueCtx
的Value
方法就可以通过Context传递数据了。
使用Context传递数据要特别注意:
1.Context是本着不可改变(immutable)的模式设计的,所以不要试图修改Context里保存的数据。
2.为了避免后续包装的键值对覆盖先前的值,最好不要直接使用string、int这类基础类型,而是用自定义类型包装一下。
下面我们来测试一下上述第二点:
package main
import (
"context"
"fmt"
)
var keyA string = "keyA"
var keyC string = "keyA"
func main() {
ctx := context.Background()
ctx1 := context.WithValue(ctx, keyA, "valueA")
fmt.Println("In ctx:")
fmt.Println("keyA => ", ctx1.Value(keyA))
ctx2 := context.WithValue(ctx1, keyC, "valueC")
fmt.Println("In ctx2:")
fmt.Println("keyA => ", ctx2.Value(keyA))
fmt.Println("keyC => ", ctx2.Value(keyC))
return
运行结果如下,ctx2中用相等的key覆盖了ctx1中的值。
In ctx:
keyA => valueA
In ctx2:
keyA => valueC
keyC => valueC
我们先来看一下ctx1和ctx2的结构,valueCtx中key和val都是interface{}类型,空接口类型的结构是一个动态类型元数据指针,外加一个指向动态值的unsafe.Pointer。
再结合valueCtx.Value方法看一下键值对查找过程:
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
可以看到,通过
ctx2.Value("keyA")
查找val时,通过比较key
,会直接锁定ctx2
中保存的val
,所以发生了Context间Value的覆盖。可以通过自定义key类型来避免这个问题。
type akeytype string
type ckeytype string
var keyA akeytype = "keyA"
var keyC ckeytype = "keyA"
......
执行结果如下:
In ctx:
keyA => valueA
In ctx2:
keyA => valueA
keyC => valueC
这一次通过ctx2.Value(keyA)查找val时,因为ctx2中存储的key与
keyA
类型不相符,所以会把查找请求委托给ctx2的parent,也就是ctx1。通过这样一级一级的向上查找,实际上形成了一个链表结构。
WithCancel、WithDeadline和WithValue函数,内部就是分别构造了cancelCtx、timerCtx和valueCtx这3种结构,最终所有的ctx将构成类似一棵树的结构,在根节点执行cancel就可以Cancel整个棵树,所以非常适合用来控制请求处理。
Go语言的http和sql包中都有Context的应用,这些待到“应用篇”再接着聊~