Go Context包源码解读

什么是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做类型断言,如果cstringer接口类型就调用cString方法

如果不是就返回用字符串表示的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为什么能写的那么优雅,有些奇思妙想真的好牛

相关推荐
2401_857622663 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589363 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
哎呦没4 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch5 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
杨哥带你写代码6 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端
AskHarries7 小时前
读《show your work》的一点感悟
后端
A尘埃7 小时前
SpringBoot的数据访问
java·spring boot·后端
yang-23077 小时前
端口冲突的解决方案以及SpringBoot自动检测可用端口demo
java·spring boot·后端
Marst Code7 小时前
(Django)初步使用
后端·python·django
代码之光_19807 小时前
SpringBoot校园资料分享平台:设计与实现
java·spring boot·后端