什么是context
go
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Context本质上是一个接口,实现这四个方法的都可以被称作context
Deadline()
返回time.Time类型的context过期时间,和一个布尔值ok。若ok为false则该context未设置过期时间
go
c := context.Background() //创建个空context
fmt.Println(c.Deadline())
c1, cancel := context.WithTimeout(c, 3*time.Second) //加上时间限制,返回一个context和一个取消函数
defer cancel()
fmt.Println(c1.Deadline())
//输出
0001-01-01 00:00:00 +0000 UTC false
2023-11-28 21:38:59.2174603 +0800 CST m=+3.002624401 true
Done()
返回一个channel,当context关闭时,channel会关闭。如果context永远不会关闭,则会返回nil
go
c := context.Background() //创建个空context
fmt.Println(c.Done())
c1, cancel := context.WithTimeout(c, 3*time.Second) //加上时间限制,返回一个context和一个取消函数
cancel()
select {
case <-c1.Done():
fmt.Println("context过期,channel关闭")
}
//输出
<nil>
context过期,channel关闭
Err()
返回一个error类型错误,没错误则返回nil,一般只有context超时和被关闭时则会返回error
go
c := context.Background() //创建个空context
c1, _ := context.WithTimeout(c, 1*time.Second) //加上时间限制,返回一个context和一个取消函数
fmt.Println(c1.Err())
time.Sleep(2*time.Second)
fmt.Println(c1.Err())
c2, cancel := context.WithTimeout(c, 3*time.Second)
cancel()
fmt.Println(c2.Err())
//输出 (context超时和被取消的返回的error不一样)
<nil>
context deadline exceeded
context canceled
Value(key any)
类似map,输入key给出对应value。key通常在全局变量中分配,可返回任何类型的值。多次调用仍会返回相同值
go
c := context.Background() //创建个空context
c1 := context.WithValue(c, "Jack", "Rose") //在context中设置一个键值对
fmt.Println(c1.Value("Jack"))
fmt.Println(c1.Value("Jack"))
fmt.Println(c1.Value("Jack"))
//输出
Rose
Rose
Rose
错误返回
取消错误
go
var Canceled = errors.New("context canceled")
当context被cancel函数关闭时调用Err()
就会返回该错误
超时错误
go
type deadlineExceededError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool { return true }
func (deadlineExceededError) Temporary() bool { return true }
此错误被集合成了一个结构体,通过调用其的三个方法来反映错误和获取信息
EmptyContext
最简单的一个context
没有过期时间和信息,用来当作父context或者其他需求,通过其不断延伸
go
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
}
emptyCtx其他方法
go
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
Background和Todo
都是emptyCtx,本质上是一样,只是人为区分他们,将他们用作不同途径。
- Background 返回一个非 nil、空的 Context。它永远不会被取消,没有价值,也没有截止日期。它通常由 main 函数、初始化和测试使用,并用作传入请求的顶级 Context。
- TODO 返回一个非 nil 的空 Context。代码应使用上下文。当不清楚要使用哪个 Context 或尚不可用时,就使用 TODO(因为周围函数尚未扩展为接受 Context 参数)。
String()
该方法用来判断该emptyCtx是Background还是Todo
cancel相关
go
type CancelFunc func()
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := withCancel(parent)
return c, func() { c.cancel(true, Canceled, nil) }
}
type CancelCauseFunc func(cause error)
func WithCancelCause(parent Context) (ctx Context, cancel CancelCauseFunc) {
c := withCancel(parent)
return c, func(cause error) { c.cancel(true, Canceled, cause) }
}
CancelFunc与CancelCauseFunc
- CancelFunc不需要参数
- CancelCauseFunc需要error类型的参数
他们都是函数的别名,起别名是为了方便后面阅读和使用
WithCancel与WithCancelCause
- 区别是Cause的携带
- 他们通过withCancel创建一个context,并返回该context和取消函数,下面以WithCancelCause为例,他返回如下的函数
go
func(cause error) { c.cancel(true, Canceled, cause) }
cause是错误原因,然后这个函数调用新创建的cancelCtx的cancel方法,完成了取消操作。cancel需要参数如下,上面传入的第二个参数是Canceled
,就是先前定义的一个错误"context canceled"
go
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error)
后面再细讲cancel
创建cancelCtx
go
func withCancel(parent Context) *cancelCtx {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, c)
return c
}
此函数需要一个父context,如果传入的是nil则会panic掉
继续往下看会发现出现新的函数newCancelCtx
,我们看看它会传给我们什么
go
func newCancelCtx(parent Context) *cancelCtx {
return &cancelCtx{Context: parent}
}
它给我们返回了一个包含父context的cancelCtx
的指针,那么cancelCtx
长什么样呢,继续跟过去
go
type cancelCtx struct {
Context
mu sync.Mutex // protects following fields
done atomic.Value
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
cause error // set to non-nil by the first cancel call
}
可以看见里面有
mu
:锁,用来保护临界资源done
:atomic包里的value类型的值,原子性我们直接看看源码
go
//Value 提供原子加载和一致类型化值的存储。
//Value 的零值从 Load 返回 nil。调用 Store 后,不得复制 Value。首次使用后不得复制 Value。
type Value struct {
v any
}
children
:字段是一个map,用于保存这个context派生出去的所有子context和它们对应的canceler。其中,canceler
接口定义如下:
go
type canceler interface {
cancel(removeFromParent bool, err error)
}
canceler
接口中有方法cancel
,用于取消与之关联的context。当一个context的父context被取消时,它会调用自己的cancel
方法将自己也取消掉,并将自己从父context的children
字段中移除。 因此,cancelCtx
中的children
字段实际上是用来记录这个context的所有子context以及它们对应的canceler
对象。当这个context被取消时,它会遍历所有的子context并调用它们的cancel
方法,以便将它们也一并取消掉。(但是每个cancelCtx自身就满足了canceler接口的条件,也就是说他们自己就是canceler类型,不是很理解具体作用)
err
:错误信息cause
:错误原因(差不多吧这两个)
现在我们已经知道什么是cancelCtx了,那么回到原函数withCanel
上来,newCancelCtx
之后是propagateCancel
函数,它的作用是将child添加到parent的children里面,让我们看看它的源码
go
func propagateCancel(parent Context, child canceler) {
done := parent.Done() //获取父context的channel来检测父context是否能被取消
//下面都用parent指代父context
if done == nil {
return // parent永远无法取消则退出
}
select {
case <-done: //监听
// parent 已经被取消
child.cancel(false, parent.Err(), Cause(parent)) //则child调用自身cancel方法取消自己
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err, p.cause)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
goroutines.Add(1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
}
}
第16行又出现了parentCancelCtx
函数,该函数作用是查找最近的父cancelCtx
go
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
return nil, false
}
return p, true
}
closedchan
在源码中是这样定义的,所以正如其名,他是个关闭的channel
go
var closedchan = make(chan struct{})
func init() {
close(closedchan)
}
所以函数中如果parent.Done()返回的是关闭的channel或者nil,说明最近cancelCtx已经被关闭,就返回nil和false
之后再通过parent.Value(&cancelCtxKey)并进行类型断言获取值,可是cancelCtxKey在包中只有潦草的定义
go
var cancelCtxKey int
是什么含义呢? 我们再去看看Value方法是怎么处理这个的
经过查找我们发现cancelCtx重写了Value
go
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
当&cancelCtxKey
作为参数则会返回该cancelCtx
可是func (c *cancelCtx) Value(key any) any
是如何获取到最近的canncelCtx呢?
如果parent本身是cancelCtx,则直接返回该parent,它就是最近的canncelCtx
如果不是,他会不断地调用上一级的value方法直到遇到canncelCtx返回为止,详见如下
go
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case *timerCtx:
if key == &cancelCtxKey {
return ctx.cancelCtx
}
c = ctx.Context
case *emptyCtx:
return nil
default:
return c.Value(key)
}
}
}
现在让我们回到parentCancelCtx
函数上来
go
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
done := parent.Done()
if done == closedchan || done == nil {
return nil, false
}
p, ok := parent.Value(&cancelCtxKey).(*cancelCtx)
if !ok {
return nil, false
}
pdone, _ := p.done.Load().(chan struct{})
if pdone != done {
return nil, false
}
return p, true
}
此时已经通过parent.Value获取到了最近的cancelCtx并传给变量p了(没有找到就return nil了)
然后,通过p.done.Load()拿到一个管道(p.done是atomic.Value类型,在atomic包里有它的Load方法), 现在讲拿到的管道(最近的cancelCtx的管道)和之前的管道作比较(parent的管道),如果不是同一个就返回nil,这个情况代表你找到了最近的自定义cancelCtx但是并不是包定义的cancelCtx
当一切都判定过去后,我们就成功拿到了最近的cancelCtx,现在我们终于可以回到propagateCancel
函数了
go
func propagateCancel(parent Context, child canceler) {
done := parent.Done() //获取父context的channel来检测父context是否能被取消
//下面都用parent指代父context
if done == nil {
return // 如果分支上不存在可cancel的context则退出
}
select {
case <-done: //监听
// parent 已经被取消
child.cancel(false, parent.Err(), Cause(parent)) //则child调用自身cancel方法取消自己
return
default:
}
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err, p.cause)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
goroutines.Add(1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
}
}
拿到后先上个锁,再将child放到children的key里
那map中的值struct{}是拿来干什么?
实际上是因为使用空结构体struct{}
作为值的好处在于它占用极少的内存空间,实际上不占用任何空间。这是因为在Go语言中,空结构体的大小是0字节。通过将空结构体作为值,我们可以实现一个只关注键的集合,而不需要额外的内存开销。
如果没有拿到,则开启一个协程来监听parent和child管道状态,若parent取消则child取消掉自己,若child先取消则不做为,当两个都没取消掉这个协程就会一直阻塞在这里,直到其中一个先cancel掉
go
goroutines.Add(1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err(), Cause(parent))
case <-child.Done():
}
}()
到此为止,一个cancelCtx就被成功创建出来了
removeChild
直接看看源码,它的作用是,从最近的父cancelCtx的children中移除child
go
func removeChild(parent Context, child canceler) {
p, ok := parentCancelCtx(parent)
if !ok {
return
}
p.mu.Lock()
if p.children != nil {
delete(p.children, child)
}
p.mu.Unlock()
}
传入一个父Ctx(不知道类型),然后根据此ctx查找最近的父cancelCtx,没有找到就return
找到就调用其锁,删掉children中的child这个key,再解锁
canceler定义
go
type canceler interface {
cancel(removeFromParent bool, err, cause error)
Done() <-chan struct{}
}
有cancel方法(后面讲)和Done方法的都是canceler,可以看到我们所有的cancelCtx都满足这个条件,所以每个cancelCtx实际上也是一个canceler
cancelCtx
cancelCtx的定义在之前已经提到过了,我们主要讲讲cancelCtx重写父Ctx的方法,就在代码旁批注解释
go
type cancelCtx struct {
Context
mu sync.Mutex
done atomic.Value
children map[canceler]struct{}
err error
cause error
}
//如果传进来的key是实现设立的cancelCtxKey则返回该cancelCtx本身
//如果不是就会一直往上找,调用该ctx存储的父ctx的信息查看key对应value的值。
func (c *cancelCtx) Value(key any) any {
if key == &cancelCtxKey {
return c
}
return value(c.Context, key)
}
func (c *cancelCtx) Done() <-chan struct{} {
//done是atomic.Value类型,负责原子性存储
//先把done里的东西通过Load取出来
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
//如果done里啥都没有就上锁(关门打狗)
c.mu.Lock()
defer c.mu.Unlock()
//再次调用Load读取,目的是再次检查context有无被取消
d = c.done.Load()
//done里确实啥也没有,就给他创建一个,然后存进去
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
//加锁读取err
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
String接口有关
go
type stringer interface {
String() string
}
以上为接口定义,在各种string方法中均有contextName函数的出现,让我们看看这是什么吧
go
func contextName(c Context) string {
if s, ok := c.(stringer); ok {
return s.String()
}
return reflectlite.TypeOf(c).String()
}
这个函数将传入的c
做类型断言,如果c
是stringer
接口类型就调用c
的String
方法
如果不是就返回用字符串表示的c
的类型
go
func (c *cancelCtx) String() string {
return contextName(c.Context) + ".WithCancel"
}
func (c *timerCtx) String() string {
return contextName(c.cancelCtx.Context) + ".WithDeadline(" +
c.deadline.String() + " [" +
time.Until(c.deadline).String() + "])"
}
func (c *valueCtx) String() string {
return contextName(c.Context) + ".WithValue(type " +
reflectlite.TypeOf(c.key).String() +
", val " + stringify(c.val) + ")"
}
以上三个String方法都是返回字符串类型的Ctx的信息
cancel函数
这个函数已经在之前出现很多次了,现在我们来详细讲讲
go
func (c *cancelCtx) cancel(removeFromParent bool, err, cause error) {
//如果没有传入err,则panic(可能是误操作的cancel,出现重大问题,直接panic掉)
if err == nil {
panic("context: internal error: missing cancel error")
}
//如果传入了err但是没有cause,就把err赋值给cause,原因就是err
if cause == nil {
cause = err
}
//之后要对Ctx里的数据操作,先上把锁
c.mu.Lock()
//如果Ctx已经被cancel掉就开锁退出
if c.err != nil {
c.mu.Unlock()
return
}
c.err = err
c.cause = cause
//关掉ctx中的管道
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
//遍历ctx的子ctx,一个一个取消,最后该分支下的全被取消掉
for child := range c.children {
child.cancel(false, err, cause)
}
c.children = nil
c.mu.Unlock()
//是否要从父ctx移除该ctx,如果传入的是就移除
if removeFromParent {
removeChild(c.Context, c)
}
}
timerCtx
重写了该方法,主要通过调用父cancelCtx的cancel方法并删掉timer
go
func (c *timerCtx) cancel(removeFromParent bool, err, cause error) {
c.cancelCtx.cancel(false, err, cause)
if removeFromParent {
removeChild(c.cancelCtx.Context, c)
}
//它的锁是用的父cancelCtx的锁
c.mu.Lock()
if c.timer != nil {
c.timer.Stop()
c.timer = nil
}
c.mu.Unlock()
}
type timerCtx struct {
*cancelCtx
timer *time.Timer
deadline time.Time
}
创建带有过期时间的Ctx
传入一个Ctx和时限,返回一个Ctx和取消它的函数
go
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
//如果没有传入parent,则报错
if parent == nil {
panic("cannot create context from nil parent")
}
//获取parent过期时间,若获取成功且此时间在设定的时间之前,那么就听parent的话,与其同时过期
//调用WithCancel,此函数会返回一个cancelCtx和取消函数
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
return WithCancel(parent)
}
//如果没有获取到parent过期时间或者获取到的时间已经过了设定时间
//就创建一个timerCtx,赋予过期时间为设定的时间
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
//创建完后要填到parent的children里
propagateCancel(parent, c)
dur := time.Until(d)
//如果已经超时,就cancel掉此ctx并从它的parent的children里移除
//再返回该ctx(???不理解这点,拿这个剥离出来的cancel掉的ctx干啥)
if dur <= 0 {
c.cancel(true, DeadlineExceeded, nil)
return c, func() { c.cancel(false, Canceled, nil) }
}
//锁住这个ctx
c.mu.Lock()
defer c.mu.Unlock()
//如果该ctx还没被cancel就等到设定时间调用cancel
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded, nil)
})
}
return c, func() { c.cancel(true, Canceled, nil) }
}
//此函数就是WithDeadline的一个补充函数,它传入的是时间段,WithDeadline传入的是时间点,效果一样
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
ValueCtx相关
ValueCtx只负责携带Key-Value键值对,其他交给父Ctx做
go
type valueCtx struct {
Context
key, val any
}
创建value
没啥好说的
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")
}
//检查这个key能不能作比较,如果不能就不能拿它当key
//为什么呢,因为如果key不能比较,我们就无法通过查找key来拿到对应的value
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
获取value
go
func (c *valueCtx) Value(key any) any {
//能直接拿到就拿
if c.key == key {
return c.val
}
//不能就往上找
return value(c.Context, key)
}
value函数
这个就像一个方法合集,通过对传入的ctx类型判断来调用相应的方法,如果在当前ctx无法取到值就会一直往上找
go
func value(c Context, key any) any {
for {
switch ctx := c.(type) {
case *valueCtx:
if key == ctx.key {
return ctx.val
}
c = ctx.Context
case *cancelCtx:
if key == &cancelCtxKey {
return c
}
c = ctx.Context
case *timerCtx:
if key == &cancelCtxKey {
return ctx.cancelCtx
}
c = ctx.Context
case *emptyCtx:
return nil
default:
return c.Value(key)
}
}
}
总结
这是我第一次阅读源码,虽然context包很简单,但是我读起来真的好吃力
读着读着总会惊叹写这些代码的人脑子是怎么长的?vocal为什么能写的那么优雅,有些奇思妙想真的好牛