小白也能看懂的 Context 包详解:从入门到精通

开发 前端
context被当作第一个参数(官方建议),并且不断透传下去,基本一个项目代码中到处都是context,但是你们真的知道它有何作用吗以及它是如何起作用的吗?

[[432383]]

前言

哈喽,大家好,我是asong。今天想与大家分享context包,经过一年的沉淀,重新出发,基于Go1.17.1从源码角度再次分析,不过这次不同的是,我打算先从入门开始,因为大多数初学的读者都想先知道怎么用,然后才会关心源码是如何实现的。

相信大家在日常工作开发中一定会看到这样的代码:

  1. func a1(ctx context ...){ 
  2.   b1(ctx) 
  3. func b1(ctx context ...){ 
  4.   c1(ctx) 
  5. func c1(ctx context ...) 

context被当作第一个参数(官方建议),并且不断透传下去,基本一个项目代码中到处都是context,但是你们真的知道它有何作用吗以及它是如何起作用的吗?我记得我第一次接触context时,同事都说这个用来做并发控制的,可以设置超时时间,超时就会取消往下执行,快速返回,我就单纯的认为只要函数中带着context参数往下传递就可以做到超时取消,快速返回。相信大多数初学者也都是和我一个想法,其实这是一个错误的思想,其取消机制采用的也是通知机制,单纯的透传并不会起作用,比如你这样写代码:

  1. func main()  { 
  2.  ctx,cancel := context.WithTimeout(context.Background(),10 * time.Second
  3.  defer cancel() 
  4.  go Monitor(ctx) 
  5.  
  6.  time.Sleep(20 * time.Second
  7.  
  8. func Monitor(ctx context.Context)  { 
  9.  for { 
  10.   fmt.Print("monitor"
  11.  } 

即使context透传下去了,没有监听取消信号也是不起任何作用的。所以了解context的使用还是很有必要的,本文就先从使用开始,逐步解析Go语言的context包,下面我们就开始喽!!!

context包的起源与作用

看官方博客我们可以知道context包是在go1.7版本中引入到标准库中的:

context可以用来在goroutine之间传递上下文信息,相同的context可以传递给运行在不同goroutine中的函数,上下文对于多个goroutine同时使用是安全的,context包定义了上下文类型,可以使用background、TODO创建一个上下文,在函数调用链之间传播context,也可以使用WithDeadline、WithTimeout、WithCancel 或 WithValue 创建的修改副本替换它,听起来有点绕,其实总结起就是一句话:context的作用就是在不同的goroutine之间同步请求特定的数据、取消信号以及处理请求的截止日期。

目前我们常用的一些库都是支持context的,例如gin、database/sql等库都是支持context的,这样更方便我们做并发控制了,只要在服务器入口创建一个context上下文,不断透传下去即可。

context的使用

创建context

context包主要提供了两种方式创建context:

  • context.Backgroud()
  • context.TODO()

这两个函数其实只是互为别名,没有差别,官方给的定义是:

  • context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来。
  • context.TODO 应该只在不确定应该使用哪种上下文时使用;

所以在大多数情况下,我们都使用context.Background作为起始的上下文向下传递。

上面的两种方式是创建根context,不具备任何功能,具体实践还是要依靠context包提供的With系列函数来进行派生:

  1. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) 
  2. func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc) 
  3. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 
  4. func WithValue(parent Context, key, val interface{}) Context 

这四个函数都要基于父Context衍生,通过这些函数,就创建了一颗Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,画个图表示一下:

基于一个父Context可以随意衍生,其实这就是一个Context树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个,每个子节点都依赖于其父节点,例如上图,我们可以基于Context.Background衍生出四个子context:ctx1.0-cancel、ctx2.0-deadline、ctx3.0-timeout、ctx4.0-withvalue,这四个子context还可以作为父context继续向下衍生,即使其中ctx1.0-cancel 节点取消了,也不影响其他三个父节点分支。

创建context方法和context的衍生方法就这些,下面我们就一个一个来看一下他们如何被使用。

WithValue携带数据

我们日常在业务开发中都希望能有一个trace_id能串联所有的日志,这就需要我们打印日志时能够获取到这个trace_id,在python中我们可以用gevent.local来传递,在java中我们可以用ThreadLocal来传递,在Go语言中我们就可以使用Context来传递,通过使用WithValue来创建一个携带trace_id的context,然后不断透传下去,打印日志时输出即可,来看使用例子:

  1. const ( 
  2.  KEY = "trace_id" 
  3.  
  4. func NewRequestID() string { 
  5.  return strings.Replace(uuid.New().String(), "-""", -1) 
  6.  
  7. func NewContextWithTraceID() context.Context { 
  8.  ctx := context.WithValue(context.Background(), KEY,NewRequestID()) 
  9.  return ctx 
  10.  
  11. func PrintLog(ctx context.Context, message string)  { 
  12.  fmt.Printf("%s|info|trace_id=%s|%s",time.Now().Format("2006-01-02 15:04:05") , GetContextValue(ctx, KEY), message) 
  13.  
  14. func GetContextValue(ctx context.Context,k string)  string{ 
  15.  v, ok := ctx.Value(k).(string) 
  16.  if !ok{ 
  17.   return "" 
  18.  } 
  19.  return v 
  20.  
  21. func ProcessEnter(ctx context.Context) { 
  22.  PrintLog(ctx, "Golang梦工厂"
  23.  
  24.  
  25. func main()  { 
  26.  ProcessEnter(NewContextWithTraceID()) 

输出结果:

  1. 2021-10-31 15:13:25|info|trace_id=7572e295351e478e91b1ba0fc37886c0|Golang梦工厂 
  2. Process finished with the exit code 0 

我们基于context.Background创建一个携带trace_id的ctx,然后通过context树一起传递,从中派生的任何context都会获取此值,我们最后打印日志的时候就可以从ctx中取值输出到日志中。目前一些RPC框架都是支持了Context,所以trace_id的向下传递就更方便了。

在使用withVaule时要注意四个事项:

  • 不建议使用context值传递关键参数,关键参数应该显示的声明出来,不应该隐式处理,context中最好是携带签名、trace_id这类值。
  • 因为携带value也是key、value的形式,为了避免context因多个包同时使用context而带来冲突,key建议采用内置类型。
  • 上面的例子我们获取trace_id是直接从当前ctx获取的,实际我们也可以获取父context中的value,在获取键值对是,我们先从当前context中查找,没有找到会在从父context中查找该键对应的值直到在某个父context中返回 nil 或者查找到对应的值。
  • context传递的数据中key、value都是interface类型,这种类型编译期无法确定类型,所以不是很安全,所以在类型断言时别忘了保证程序的健壮性。

超时控制

通常健壮的程序都是要设置超时时间的,避免因为服务端长时间响应消耗资源,所以一些web框架或rpc框架都会采用withTimeout或者withDeadline来做超时控制,当一次请求到达我们设置的超时时间,就会及时取消,不在往下执行。withTimeout和withDeadline作用是一样的,就是传递的时间参数不同而已,他们都会通过传入的时间来自动取消Context,这里要注意的是他们都会返回一个cancelFunc方法,通过调用这个方法可以达到提前进行取消,不过在使用的过程还是建议在自动取消后也调用cancelFunc去停止定时减少不必要的资源浪费。

withTimeout、WithDeadline不同在于WithTimeout将持续时间作为参数输入而不是时间对象,这两个方法使用哪个都是一样的,看业务场景和个人习惯了,因为本质withTimout内部也是调用的WithDeadline。

现在我们就举个例子来试用一下超时控制,现在我们就模拟一个请求写两个例子:

  • 达到超时时间终止接下来的执行
  1. func main()  { 
  2.  HttpHandler() 
  3.  
  4. func NewContextWithTimeout() (context.Context,context.CancelFunc) { 
  5.  return context.WithTimeout(context.Background(), 3 * time.Second
  6.  
  7. func HttpHandler()  { 
  8.  ctx, cancel := NewContextWithTimeout() 
  9.  defer cancel() 
  10.  deal(ctx) 
  11.  
  12. func deal(ctx context.Context)  { 
  13.  for i:=0; i< 10; i++ { 
  14.   time.Sleep(1*time.Second
  15.   select { 
  16.   case <- ctx.Done(): 
  17.    fmt.Println(ctx.Err()) 
  18.    return 
  19.   default
  20.    fmt.Printf("deal time is %d\n", i) 
  21.   } 
  22.  } 

输出结果:

  1. deal time is 0 
  2. deal time is 1 
  3. context deadline exceeded 
  • 没有达到超时时间终止接下来的执行
  1. func main()  { 
  2.  HttpHandler1() 
  3.  
  4. func NewContextWithTimeout1() (context.Context,context.CancelFunc) { 
  5.  return context.WithTimeout(context.Background(), 3 * time.Second
  6.  
  7. func HttpHandler1()  { 
  8.  ctx, cancel := NewContextWithTimeout1() 
  9.  defer cancel() 
  10.  deal1(ctx, cancel) 
  11.  
  12. func deal1(ctx context.Context, cancel context.CancelFunc)  { 
  13.  for i:=0; i< 10; i++ { 
  14.   time.Sleep(1*time.Second
  15.   select { 
  16.   case <- ctx.Done(): 
  17.    fmt.Println(ctx.Err()) 
  18.    return 
  19.   default
  20.    fmt.Printf("deal time is %d\n", i) 
  21.    cancel() 
  22.   } 
  23.  } 

输出结果:

  1. deal time is 0 
  2. context canceled 

使用起来还是比较容易的,既可以超时自动取消,又可以手动控制取消。这里大家要记的一个坑,就是我们往从请求入口透传的调用链路中的context是携带超时时间的,如果我们想在其中单独开一个goroutine去处理其他的事情并且不会随着请求结束后而被取消的话,那么传递的context要基于context.Background或者context.TODO重新衍生一个传递,否决就会和预期不符合了,可以看一下我之前的一篇踩坑文章:context使用不当引发的一个bug。

withCancel取消控制

日常业务开发中我们往往为了完成一个复杂的需求会开多个gouroutine去做一些事情,这就导致我们会在一次请求中开了多个goroutine确无法控制他们,这时我们就可以使用withCancel来衍生一个context传递到不同的goroutine中,当我想让这些goroutine停止运行,就可以调用cancel来进行取消。

来看一个例子:

  1. func main()  { 
  2.  ctx,cancel := context.WithCancel(context.Background()) 
  3.  go Speak(ctx) 
  4.  time.Sleep(10*time.Second
  5.  cancel() 
  6.  time.Sleep(1*time.Second
  7.  
  8. func Speak(ctx context.Context)  { 
  9.  for range time.Tick(time.Second){ 
  10.   select { 
  11.   case <- ctx.Done(): 
  12.    fmt.Println("我要闭嘴了"
  13.    return 
  14.   default
  15.    fmt.Println("balabalabalabala"
  16.   } 
  17.  } 

运行结果:

  1. balabalabalabala 
  2. ....省略 
  3. balabalabalabala 
  4. 我要闭嘴了 

我们使用withCancel创建一个基于Background的ctx,然后启动一个讲话程序,每隔1s说一话,main函数在10s后执行cancel,那么speak检测到取消信号就会退出。

自定义Context

因为Context本质是一个接口,所以我们可以通过实现Context达到自定义Context的目的,一般在实现Web框架或RPC框架往往采用这种形式,比如gin框架的Context就是自己有封装了一层,具体代码和实现就贴在这里,有兴趣可以看一下gin.Context是如何实现的。

源码赏析

Context其实就是一个接口,定义了四个方法:

  1. type Context interface { 
  2.  Deadline() (deadline time.Time, ok bool) 
  3.  Done() <-chan struct{} 
  4.  Err() error 
  5.  Value(key interface{}) interface{} 
  • Deadlne方法:当Context自动取消或者到了取消时间被取消后返回
  • Done方法:当Context被取消或者到了deadline返回一个被关闭的channel
  • Err方法:当Context被取消或者关闭后,返回context取消的原因
  • Value方法:获取设置的key对应的值

这个接口主要被三个类继承实现,分别是emptyCtx、ValueCtx、cancelCtx,采用匿名接口的写法,这样可以对任意实现了该接口的类型进行重写。

下面我们就从创建到使用来层层分析。

创建根Context

其在我们调用context.Background、context.TODO时创建的对象就是empty:

  1. var ( 
  2.  background = new(emptyCtx) 
  3.  todo       = new(emptyCtx) 
  4.  
  5. func Background() Context { 
  6.  return background 
  7.  
  8. func TODO() Context { 
  9.  return todo 

Background和TODO还是一模一样的,官方说:background它通常由主函数、初始化和测试使用,并作为传入请求的顶级上下文;TODO是当不清楚要使用哪个 Context 或尚不可用时,代码应使用 context.TODO,后续在在进行替换掉,归根结底就是语义不同而已。

emptyCtx类

emptyCtx主要是给我们创建根Context时使用的,其实现方法也是一个空结构,实际源代码长这样:

  1. type emptyCtx int 
  2.  
  3. func (*emptyCtx) Deadline() (deadline time.Time, ok bool) { 
  4.  return 
  5.  
  6. func (*emptyCtx) Done() <-chan struct{} { 
  7.  return nil 
  8.  
  9. func (*emptyCtx) Err() error { 
  10.  return nil 
  11.  
  12. func (*emptyCtx) Value(key interface{}) interface{} { 
  13.  return nil 
  14.  
  15. func (e *emptyCtx) String() string { 
  16.  switch e { 
  17.  case background: 
  18.   return "context.Background" 
  19.  case todo: 
  20.   return "context.TODO" 
  21.  } 
  22.  return "unknown empty Context" 

WithValue的实现

withValue内部主要就是调用valueCtx类:

  1. func WithValue(parent Context, key, val interface{}) Context { 
  2.  if parent == nil { 
  3.   panic("cannot create context from nil parent"
  4.  } 
  5.  if key == nil { 
  6.   panic("nil key"
  7.  } 
  8.  if !reflectlite.TypeOf(key).Comparable() { 
  9.   panic("key is not comparable"
  10.  } 
  11.  return &valueCtx{parent, key, val} 

valueCtx类

valueCtx目的就是为Context携带键值对,因为它采用匿名接口的继承实现方式,他会继承父Context,也就相当于嵌入Context当中了

  1. type valueCtx struct { 
  2.  Context 
  3.  key, val interface{} 

实现了String方法输出Context和携带的键值对信息:

  1. func (c *valueCtx) String() string { 
  2.  return contextName(c.Context) + ".WithValue(type " + 
  3.   reflectlite.TypeOf(c.key).String() + 
  4.   ", val " + stringify(c.val) + ")" 

实现Value方法来存储键值对:

  1. func (c *valueCtx) Value(key interface{}) interface{} { 
  2.  if c.key == key { 
  3.   return c.val 
  4.  } 
  5.  return c.Context.Value(key

看图来理解一下:

所以我们在调用Context中的Value方法时会层层向上调用直到最终的根节点,中间要是找到了key就会返回,否会就会找到最终的emptyCtx返回nil。

WithCancel的实现

我们来看一下WithCancel的入口函数源代码:

  1. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) { 
  2.  if parent == nil { 
  3.   panic("cannot create context from nil parent"
  4.  } 
  5.  c := newCancelCtx(parent) 
  6.  propagateCancel(parent, &c) 
  7.  return &c, func() { c.cancel(true, Canceled) } 

这个函数执行步骤如下:

  • 创建一个cancelCtx对象,作为子context
  • 然后调用propagateCancel构建父子context之间的关联关系,这样当父context被取消时,子context也会被取消。
  • 返回子context对象和子树取消函数

我们先分析一下cancelCtx这个类。

cancelCtx类

cancelCtx继承了Context,也实现了接口canceler:

  1. type cancelCtx struct { 
  2.  Context 
  3.  
  4.  mu       sync.Mutex            // protects following fields 
  5.  done     atomic.Value          // of chan struct{}, created lazily, closed by first cancel call 
  6.  children map[canceler]struct{} // set to nil by the first cancel call 
  7.  err      error                 // set to non-nil by the first cancel call 

字短解释:

  • mu:就是一个互斥锁,保证并发安全的,所以context是并发安全的
  • done:用来做context的取消通知信号,之前的版本使用的是chan struct{}类型,现在用atomic.Value做锁优化
  • children:key是接口类型canceler,目的就是存储实现当前canceler接口的子节点,当根节点发生取消时,遍历子节点发送取消信号
  • error:当context取消时存储取消信息

这里实现了Done方法,返回的是一个只读的channel,目的就是我们在外部可以通过这个阻塞的channel等待通知信号。

具体代码就不贴了。我们先返回去看propagateCancel是如何做构建父子Context之间的关联。

propagateCancel方法

代码有点长,解释有点麻烦,我把注释添加到代码中看起来比较直观:

  1. func propagateCancel(parent Context, child canceler) { 
  2.   // 如果返回nil,说明当前父`context`从来不会被取消,是一个空节点,直接返回即可。 
  3.  done := parent.Done() 
  4.  if done == nil { 
  5.   return // parent is never canceled 
  6.  } 
  7.  
  8.   // 提前判断一个父context是否被取消,如果取消了也不需要构建关联了, 
  9.   // 把当前子节点取消掉并返回 
  10.  select { 
  11.  case <-done: 
  12.   // parent is already canceled 
  13.   child.cancel(false, parent.Err()) 
  14.   return 
  15.  default
  16.  } 
  17.  
  18.   // 这里目的就是找到可以“挂”、“取消”的context 
  19.  if p, ok := parentCancelCtx(parent); ok { 
  20.   p.mu.Lock() 
  21.     // 找到了可以“挂”、“取消”的context,但是已经被取消了,那么这个子节点也不需要 
  22.     // 继续挂靠了,取消即可 
  23.   if p.err != nil { 
  24.    child.cancel(false, p.err) 
  25.   } else { 
  26.       // 将当前节点挂到父节点的childrn map中,外面调用cancel时可以层层取消 
  27.    if p.children == nil { 
  28.         // 这里因为childer节点也会变成父节点,所以需要初始化map结构 
  29.     p.children = make(map[canceler]struct{}) 
  30.    } 
  31.    p.children[child] = struct{}{} 
  32.   } 
  33.   p.mu.Unlock() 
  34.  } else { 
  35.     // 没有找到可“挂”,“取消”的父节点挂载,那么就开一个goroutine 
  36.   atomic.AddInt32(&goroutines, +1) 
  37.   go func() { 
  38.    select { 
  39.    case <-parent.Done(): 
  40.     child.cancel(false, parent.Err()) 
  41.    case <-child.Done(): 
  42.    } 
  43.   }() 
  44.  } 

这段代码真正产生疑惑的是这个if、else分支。不看代码了,直接说为什么吧。因为我们可以自己定制context,把context塞进一个结构时,就会导致找不到可取消的父节点,只能重新起一个协程做监听。

对这块有迷惑的推荐阅读饶大大文章:[深度解密Go语言之context](https://www.cnblogs.com/qcrao-2018/p/11007503.html),定能为你排忧解惑。

cancel方法

最后我们再来看一下返回的cancel方法是如何实现,这个方法会关闭上下文中的 Channel 并向所有的子上下文同步取消信号:

  1. func (c *cancelCtx) cancel(removeFromParent bool, err error) { 
  2.   // 取消时传入的error信息不能为nil, context定义了默认error:var Canceled = errors.New("context canceled"
  3.  if err == nil { 
  4.   panic("context: internal error: missing cancel error"
  5.  } 
  6.   // 已经有错误信息了,说明当前节点已经被取消过了 
  7.  c.mu.Lock() 
  8.  if c.err != nil { 
  9.   c.mu.Unlock() 
  10.   return // already canceled 
  11.  } 
  12.    
  13.  c.err = err 
  14.   // 用来关闭channel,通知其他协程 
  15.  d, _ := c.done.Load().(chan struct{}) 
  16.  if d == nil { 
  17.   c.done.Store(closedchan) 
  18.  } else { 
  19.   close(d) 
  20.  } 
  21.   // 当前节点向下取消,遍历它的所有子节点,然后取消 
  22.  for child := range c.children { 
  23.   // NOTE: acquiring the child's lock while holding parent's lock. 
  24.   child.cancel(false, err) 
  25.  } 
  26.   // 节点置空 
  27.  c.children = nil 
  28.  c.mu.Unlock() 
  29.   // 把当前节点从父节点中移除,只有在外部父节点调用时才会传true 
  30.   // 其他都是传false,内部调用都会因为c.children = nil被剔除出去 
  31.  if removeFromParent { 
  32.   removeChild(c.Context, c) 
  33.  } 

到这里整个WithCancel方法源码就分析好了,通过源码我们可以知道cancel方法可以被重复调用,是幂等的。

withDeadline、WithTimeout的实现

先看WithTimeout方法,它内部就是调用的WithDeadline方法:

  1. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) { 
  2.  return WithDeadline(parent, time.Now().Add(timeout)) 

所以我们重点来看withDeadline是如何实现的:

  1. func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) { 
  2.   // 不能为空`context`创建衍生context 
  3.  if parent == nil { 
  4.   panic("cannot create context from nil parent"
  5.  } 
  6.    
  7.   // 当父context的结束时间早于要设置的时间,则不需要再去单独处理子节点的定时器了 
  8.  if cur, ok := parent.Deadline(); ok && cur.Before(d) { 
  9.   // The current deadline is already sooner than the new one. 
  10.   return WithCancel(parent) 
  11.  } 
  12.   // 创建一个timerCtx对象 
  13.  c := &timerCtx{ 
  14.   cancelCtx: newCancelCtx(parent), 
  15.   deadline:  d, 
  16.  } 
  17.   // 将当前节点挂到父节点上 
  18.  propagateCancel(parent, c) 
  19.    
  20.   // 获取过期时间 
  21.  dur := time.Until(d) 
  22.   // 当前时间已经过期了则直接取消 
  23.  if dur <= 0 { 
  24.   c.cancel(true, DeadlineExceeded) // deadline has already passed 
  25.   return c, func() { c.cancel(false, Canceled) } 
  26.  } 
  27.  c.mu.Lock() 
  28.  defer c.mu.Unlock() 
  29.   // 如果没被取消,则直接添加一个定时器,定时去取消 
  30.  if c.err == nil { 
  31.   c.timer = time.AfterFunc(dur, func() { 
  32.    c.cancel(true, DeadlineExceeded) 
  33.   }) 
  34.  } 
  35.  return c, func() { c.cancel(true, Canceled) } 

withDeadline相较于withCancel方法也就多了一个定时器去定时调用cancel方法,这个cancel方法在timerCtx类中进行了重写,我们先来看一下timerCtx类,他是基于cancelCtx的,多了两个字段:

  1. type timerCtx struct { 
  2.  cancelCtx 
  3.  timer *time.Timer // Under cancelCtx.mu. 
  4.  
  5.  deadline time.Time 

timerCtx实现的cancel方法,内部也是调用了cancelCtx的cancel方法取消:

  1. func (c *timerCtx) cancel(removeFromParent bool, err error) { 
  2.   // 调用cancelCtx的cancel方法取消掉子节点context 
  3.  c.cancelCtx.cancel(false, err) 
  4.   // 从父context移除放到了这里来做 
  5.  if removeFromParent { 
  6.   // Remove this timerCtx from its parent cancelCtx's children. 
  7.   removeChild(c.cancelCtx.Context, c) 
  8.  } 
  9.   // 停掉定时器,释放资源 
  10.  c.mu.Lock() 
  11.  if c.timer != nil { 
  12.   c.timer.Stop() 
  13.   c.timer = nil 
  14.  } 
  15.  c.mu.Unlock() 

终于源码部分我们就看完了,现在你何感想?

context的优缺点

context包被设计出来就是做并发控制的,这个包有利有弊,个人总结了几个优缺点,欢迎评论区补充。

缺点

  • 影响代码美观,现在基本所有web框架、RPC框架都是实现了context,这就导致我们的代码中每一个函数的一个参数都是context,即使不用也要带着这个参数透传下去,个人觉得有点丑陋。
  • context可以携带值,但是没有任何限制,类型和大小都没有限制,也就是没有任何约束,这样很容易导致滥用,程序的健壮很难保证;还有一个问题就是通过context携带值不如显式传值舒服,可读性变差了。
  • 可以自定义context,这样风险不可控,更加会导致滥用。
  • context取消和自动取消的错误返回不够友好,无法自定义错误,出现难以排查的问题时不好排查。
  • 创建衍生节点实际是创建一个个链表节点,其时间复杂度为O(n),节点多了会掉支效率变低。

优点

使用context可以更好的做并发控制,能更好的管理goroutine滥用。

context的携带者功能没有任何限制,这样我我们传递任何的数据,可以说这是一把双刃剑

网上都说context包解决了goroutine的cancelation问题,你觉得呢?

参考文章

https://pkg.go.dev/context@go1.7beta1#Background https://studygolang.com/articles/21531 https://draveness.me/golang/docs/part3-runtime/ch06-concurrency/golang-context/ https://www.cnblogs.com/qcrao-2018/p/11007503.html https://segmentfault.com/a/1190000039294140 https://www.flysnow.org/2017/05/12/go-in-action-go-context.html

总结

context虽然在使用上丑陋了一点,但是他却能解决很多问题,日常业务开发中离不开context的使用,不过也别使用错了context,其取消也采用的channel通知,所以代码中还有要有监听代码来监听取消信号,这点也是经常被广大初学者容易忽视的一个点。

文中示例已上传github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/context_example

 

好啦,本文到这里就结束了,我是asong,我们下期见。

 

责任编辑:武晓燕 来源: Golang梦工厂
相关推荐

2017-02-22 15:04:52

2024-11-01 05:10:00

2019-12-27 09:47:05

大数据TomcatWeb

2024-01-19 13:39:00

死锁框架排查

2020-02-15 17:16:05

Kubernetes容器

2019-11-18 10:38:03

线程池Java框架

2019-10-10 11:10:04

SpringBoot异步编程

2018-12-24 08:46:52

Kubernetes对象模型

2022-07-04 08:31:42

GitOpsGit基础设施

2019-03-26 11:15:34

AI机器学习人工智能

2013-09-22 10:34:08

码农机器学习算法

2020-03-31 10:36:07

数据平台架构

2018-05-16 10:07:02

监控报警系统

2019-11-04 13:09:43

数据平台架构

2017-11-02 12:08:56

2020-11-16 16:38:30

人工智能AI

2021-09-27 13:50:13

Python装饰器函数

2022-10-11 08:27:45

Spring事务管理性能统计

2018-03-06 10:38:23

云计算大数据人工智能

2018-11-19 08:34:22

Hadoop架构HDFS
点赞
收藏

51CTO技术栈公众号