golang io.copy调用多次_深入Golang系列(一)Context

 Golang让开发变得高效有趣已经成为一个不争的事实。事实上,极低的开发门槛带来的最大的问题在于,开发者甚至不需要对一个概念(包)进行过多的了解即可写出满足需要的程序。长久以往,写出的程序在健壮性,可维护性上都大打折扣。

本篇作为深入Golang系列的一个开篇,将从实际开发入手,对于平日常见的,却又用不好的包:context,进行一个全面的割析,希望能够对今后的开发工作有所帮助。

01

啥是Context

首先,context包是在golang1.7版本中加入进来的特性,它的加入是为了简化处理请求之间调用所传递的上下文信息,这个信息包括了截止时间,超时时间,取消信号,和额外的请求范围的值。

理论上讲,对server发起一个请求时,就应该创建一个Context对象,并随着调用请求将这个对象传递给server;同样,server需要接受这个Context,并按需求做出相应的处理。如果是在一个函数调用链中,servers必须传递Context,甚至可选的,使用包装函数WithCancel,WithDeadline,WithTimeout或WithValue来给原始的Context赋予更多的信息和能力。WithCancel,WithDeadline,WithTimeout或WithValue这些包装函数都接受一个父Context对象,并针对不同的需求包装成一个子Context返回,同时返回的还有一个CancelFunc函数,调用CancelFunc会取消子项及其子项,删除父项对子项的引用,并停止任何关联的定时器。未能调用CancelFunc会泄漏子项及其子项,直到取消父项或计时器触发。

WithCancel的实现举例:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {   c := newCancelCtx(parent)   propagateCancel(parent, &c)   return &c, func() { c.cancel(true, Canceled) }}

官方提出的一些需要遵循的规范:

  • 不要将Context存储在结构类型中;相反地,将Context明确传递给需要它的每个函数。Context应该是第一个参数,通常命名为ctx:

  • func DoSomething(ctx context.Context, arg Arg) error {  // ... use ctx ...}
  • 即使函数允许,也不要传递nil Context。如果不确定要使用哪个上下文,传递context.TODO。

  • 仅将Context中的Value用于转换进程和API的请求范围内的数据,而不是将可选参数传递给函数。

  • 可以将相同的Context传递给在不同goroutine中运行的函数;Context对于多个goroutine同时使用是安全的。

02

走进源码

Context包的位置:GO_SDK/src/context/,这个包中除了context.go剩下的都是测试文件。

在context.go中可以发现我们今天的主角Context,其是一个拥有四个方法的接口:

type Context interface {   // Deadline返回一个时间和一个状态布尔变量ok,这个时间表示工作   // 完成的最后期限,到期后当前context实例被取消掉。    // 如果ok==false代表当前context并没有设置deadline。   // 无论多少次调用Deadline()都将返回相同的结果。   Deadline() (deadline time.Time, ok bool)   // Done 返回一个channel,这个channel在context被取消的时候被   // 关闭。   // 当这个context无法被取消的时候,Done 可能返回nil。   // 多次调用Done()返回的都是同一个值.   //   // WithCancel 让Done通道在cancel函数被调用时关闭   // WithDeadline 让Done通道在deadline到期时关闭   // WithTimeout 让Done通道在超时时间到时关闭   //   // Done的一个推荐用法:   //   //  // Stream 是一个负责通过DoSomething函数生成值的一个函数,   //  // 被调用之后,它会一直给out channel发送生成的值,   //  // 直到DoSomething 返回一个错误或者ctx.Done的通道被关闭。   //  func Stream(ctx context.Context, out chan   //     for {   //        v, err := DoSomething(ctx)   //        if err != nil {   //           return err   //        }   //        select {   //        case    //           return ctx.Err()   //        case out    //        }   //     }   //  }   //   Done() struct{}   // 如果Done还没有被closed, Err返回值为nil.   // 如果Done已经被closed, Err返回一个非空error,它会解释退出   // 原因:    // 被取消了如果context被取消或者DeadlineExceeded了如果   // deadline到了。   // 一旦Err返回了一个非空error,之后无论调用多少次Err都会得到   // 同样的错误   Err() error   // Value返回与key的context关联的值,或者为nil,如果没有值与   // 键相关联。   // 连续调用Value,传入相同的键返回始终相同的结果。   //   // 在一个context中key唯一指向一个特定的value。   // key可以是任意支持比较相等性的值;开发者的包应该负责将key定义   // 为不可导出类型来避免碰撞。   Value(key interface{}) interface{}}

因此,任何实现了上述四个方法的实体都是一个Context(Interface)。


为了方便用户使用,context包中已经预实现了两个Context:

var (   background = new(emptyCtx)   todo       = new(emptyCtx))

emptyCtx其实底层类型就是int:

// 一个emptyCtx永远不会被取消,没有任何的value,并且没有一个// deadline。// 它不是定义为一个空struct{},的原因在于这个类型的变量必须拥有// 彼此独立不相等的地址。type emptyCtx int

由以上源码可以看出,每一次调用context.Background()都会返回一个新的emptyCtx实例,且拥有自己的地址空间。

emptyCtx所实现的Context接口的方法非常简单:

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 interface{}) interface{} {   return nil}

可以看出,emptyCtx的确仅仅是实现了Context接口,但它永远不会被取消,没有任何的value,并且没有一个deadline。


要使用一个能够传递控制信息(cancel、deadline)及数据信息(value)的context,那就一定是在原始的context实例基础上包装所需要的能力,这个过程对应到的就是四个with函数:WithCancel,WithDeadline,WithTimeout及WithValue。

前文已经举出了WithCancel的源码实例:

func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {   c := newCancelCtx(parent)   propagateCancel(parent, &c)   return &c, func() { c.cancel(true, Canceled) }}

关键看看newCancelCtx这个函数:

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

原来WithCancel调用之后,返回的已经不是原来传入的那个context了,取而代之的组合了原本的context并且根据With的类型派生出的新的子context:在这里是cancelCtx,相应的还有timerCtx,vakueCtx。对应的源码如下:

type cancelCtx struct {   Context   mu       sync.Mutex               done     chan struct{}            children map[canceler]struct{}    err      error                }type timerCtx struct {   cancelCtx   timer *time.Timer    deadline time.Time}type valueCtx struct {   Context   key, val interface{}}

值得注意的是timerCtx其实内嵌了cancelCtx,因此timerCtx天然也具有cancelCtx的所有性质。

ok,以上基本上就是context源码的主要细节,更详细的内容可以直接浏览go doc官网。

了解了这么多,接下来看看context在实际的开发中如何使用。

03

Context应用举例

由于原始的context实现(emptyCtx)没有任何的额外特性,因此下面的例子由官方提供的包装函数(WithCancel,WithDeadline,WithTimeout及WithValue)展开。

WithCancel


这个例子演示了使用可取消的context来防止goroutine泄漏。在示例函数结束时,gen启动的goroutine将返回而不会泄漏。

package mainimport (  "context"  "fmt")func main() {  // gen函数在一个独立的goroutine不断生产int数,并且将产生出来  // 的int数传递到一个无缓冲的channel中。  // 调用gen函数的函数需要在完成所需的处理之后取消产生int数的  // goroutine,来防止gen启动的内部goroutine泄露。  gen := func(ctx context.Context) chan int {    dst := make(chan int)    n := 1    go func() {      for {        select {        case           return // 返回能够避免goroutine泄露        case dst           n++        }      }    }()    return dst  }  ctx, cancel := context.WithCancel(context.Background())  defer cancel() // 延迟到main函数结束时调用  for n := range gen(ctx) {    fmt.Println(n)    if n == 5 {      break    }  }}

WithDeadline


这个例子传递一个带有任意截止日期的context来告诉阻塞函数它应该在deadline到来时放弃它的工作。

package mainimport (  "context"  "fmt"  "time")func main() {  d := time.Now().Add(50 * time.Millisecond)  ctx, cancel := context.WithDeadline(context.Background(), d)  // 虽然ctx会按需超期并触发cancel函数, 但是在任何时候调用  // cancel()函数主动取消context仍然是一个最佳实践。  // 如果不这样做可能会保留context及其父级的活动时间超过必要时间。  defer cancel()  select {  case After(1 * time.Second):    fmt.Println("overslept")  case Done():    fmt.Println(ctx.Err())  }}

WithTimeout


这个例子传递具有超时的context,以告知阻塞函数在超时过后它应该放弃其工作。

package mainimport (  "context"  "fmt"  "time")func main() {  // 传递一个带有timeout的context能够告知一个blocking函数当  // timeout超时时退出工作。  // 这个例子和deadline的很像,区别在于一个是传未来的一个时间点,  // 一个是传一段时间.  ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)  defer cancel()  select {  case 1 * time.Second):    fmt.Println("overslept")  case     fmt.Println(ctx.Err()) // prints "context deadline exceeded"  }}

WithValue


这个例子演示如何将值传递给上下文以及如何检索它(如果存在)。

package mainimport (  "context"  "fmt")func main() {  type favContextKey string  f := func(ctx context.Context, k favContextKey) {    if v := ctx.Value(k); v != nil {      fmt.Println("found value:", v)      return    }    fmt.Println("key not found:", k)  }  k := favContextKey("language")  ctx := context.WithValue(context.Background(), k, "Go")  f(ctx, k)  f(ctx, favContextKey("color"))}

       719e17834f176c09420c7119ee593094.png

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值