什么是context

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未设置过期时刻

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

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

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通常在全局变量中分配,可回来任何类型的值。屡次调用仍会回来相同值

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

过错回来

撤销过错
var Canceled = errors.New("context canceled")

当context被cancel函数封闭时调用Err()就会回来该过错

超时过错
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或许其他需求,经过其不断延伸

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其他办法

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相关

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为例,他回来如下的函数
func(cause error) { c.cancel(true, Canceled, cause) }

cause是过错原因,然后这个函数调用新创立的cancelCtx的cancel办法,完成了撤销操作。cancel需求参数如下,上面传入的第二个参数是Canceled,便是从前界说的一个过错"context canceled"

func (c *cancelCtx) cancel(removeFromParent bool, err, cause error)

后边再细讲cancel

创立cancelCtx

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,咱们看看它会传给咱们什么

func newCancelCtx(parent Context) *cancelCtx {
   return &cancelCtx{Context: parent}
}

它给咱们回来了一个包括父context的cancelCtx的指针,那么cancelCtx长什么样呢,持续跟曩昔

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类型的值,原子性咱们直接看看源码
//Value 提供原子加载和共同类型化值的存储。
//Value 的零值从 Load 回来 nil。调用 Store 后,不得仿制 Value。首次运用后不得仿制 Value。
type Value struct {
   v any
}
  • children:字段是一个map,用于保存这个context派生出去的一切子context和它们对应的canceler。其间,canceler接口界说如下:
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里面,让咱们看看它的源码

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

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

var closedchan = make(chan struct{})
func init() {
   close(closedchan)
}

所以函数中假如parent.Done()回来的是封闭的channel或许nil,说明最近cancelCtx现已被封闭,就回来nil和false
之后再经过parent.Value(&cancelCtxKey)并进行类型断语获取值,可是cancelCtxKey在包中只要潦草的界说

var cancelCtxKey int

是什么含义呢?
咱们再去看看Value办法是怎样处理这个的
经过查找咱们发现cancelCtx重写了Value

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回来停止,详见如下

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函数上来

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函数了

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掉

goroutines.Add(1)
go func() {
   select {
   case <-parent.Done():
      child.cancel(false, parent.Err(), Cause(parent))
   case <-child.Done():
   }
}()

到此停止,一个cancelCtx就被成功创立出来了

removeChild

直接看看源码,它的作用是,从最近的父cancelCtx的children中移除child

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界说

type canceler interface {
   cancel(removeFromParent bool, err, cause error)
   Done() <-chan struct{}
}

有cancel办法(后边讲)和Done办法的都是canceler,能够看到咱们一切的cancelCtx都满意这个条件,所以每个cancelCtx实际上也是一个canceler

cancelCtx

cancelCtx的界说在之前现已提到过了,咱们首要讲讲cancelCtx重写父Ctx的办法,就在代码旁批注解说

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接口有关

type stringer interface {
   String() string
}

以上为接口界说,在各种string办法中均有contextName函数的呈现,让咱们看看这是什么吧

func contextName(c Context) string {
   if s, ok := c.(stringer); ok {
      return s.String()
   }
   return reflectlite.TypeOf(c).String()
}

这个函数将传入的c做类型断语,假如cstringer接口类型就调用cString办法
假如不是就回来用字符串表明的c的类型

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函数

这个函数现已在之前呈现很屡次了,现在咱们来详细讲讲

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

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和撤销它的函数

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做

type valueCtx struct {
   Context
   key, val any
}

创立value

没啥好说的

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

func (c *valueCtx) Value(key any) any {
   //能直接拿到就拿
   if c.key == key {
      return c.val
   }
   //不能就往上找
   return value(c.Context, key)
}

value函数

这个就像一个办法合集,经过对传入的ctx类型判别来调用相应的办法,假如在当前ctx无法取到值就会一向往上找

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为什么能写的那么高雅,有些奇思妙想真的好牛