Golang Context 详细介绍

Stella981
• 阅读 460

Golang context

  本文包含对context实现上的分析和使用方式,分析部分源码讲解比价多,可能会比较枯燥,读者可以直接跳过去阅读使用部分。

  ps: 作者本着开源分享的精神撰写本篇文章,如果出现任何误差务必留言指正,作者会在第一时间内修正,共同维护一个好的开源生态,谢谢!!!

一、简介

作者所讲的context的包名称是: "golang.org/x/net/context" ,希望读者不要引用错误了。

在godoc中对context的介绍如下:

Golang Context 详细介绍 Golang Context 详细介绍

 Package context defines the Context type, which carries deadlines, cancelation signals, and other request-scoped values across API boundaries and between processes.

 As of Go 1.7 this package is available in the standard library under the name context. https://golang.org/pkg/context. 

Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context. The chain of function calls between must propagate the Context, optionally replacing it with a modified copy created using WithDeadline, WithTimeout, WithCancel, or WithValue.

Programs that use Contexts should follow these rules to keep interfaces consistent across packages and enable static analysis tools to check context propagation:

Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it. The Context should be the first parameter, typically named ctx:

func DoSomething(ctx context.Context, arg Arg) error {
    // ... use ctx ...
}

Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.

Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.

The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.

See http://blog.golang.org/context for example code for a server that uses Contexts.

View Code

 鉴于作者英文水平有限,在这里不进行对照翻译,以免误导读者。它的第一句已经介绍了它的作用了:一个贯穿API的边界和进程之间的context 类型,可以携带deadlines、cancel signals和其他信息。就如同它的中文翻译一样:上下文。在一个应用服务中会并行运行很多的goroutines或进程, 它们彼此之间或者是从属关系、竞争关系、互斥关系,不同的goroutines和进程进行交互的时候需要进行状态的切换和数据的同步,而这就是context包要支持的功能。

二、解析

context的接口定义如下:

   每一个接口都有详细的注释,这里就不重复了。 在context的源码中有以下几个结构体实现了Context Interface:

// A Context carries a deadline, a cancelation signal, and other values across
// API boundaries.Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
   // Deadline returns the time when work done on behalf of this context
   // should be canceled.  Deadline returns ok==false when no deadline is
   // set.  Successive calls to Deadline return the same results.
   Deadline() (deadline time.Time, ok bool)
   // Done returns a channel that's closed when work done on behalf of this
   // context should be canceled.  Done may return nil if this context can
   // never be canceled.  Successive calls to Done return the same value.
   Done() <-chan struct{}
   // Err returns a non-nil error value after Done is closed.  Err returns
   // Canceled if the context was canceled or DeadlineExceeded if the
   // context's deadline passed.  No other values for Err are defined.
   // After Done is closed, successive calls to Err return the same value.
   Err() error
   // Value returns the value associated with this context for key, or nil
   // if no value is associated with key.  Successive calls to Value with
   // the same key returns the same result.
   Value(key interface{}) interface{}
}

2.1 empty context

// An emptyCtx is never canceled, has no values, and has no deadline. It is not
// struct{}, since vars of this type must have distinct addresses.
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 interface{}) interface{} {
    return nil
}

func (e *emptyCtx) String() string {
    switch e {
    case background:
        return "context.Background"
    case todo:
        return "context.TODO"
    }
    return "unknown empty Context"
}

  这是一个空的ctx类型,每一个返回值都为空,它什么都功能都不具备,主要的作用是作为所有的context类型的起始点,**context.Background()**函数返回的就是这中类型的Context:

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

// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
    return background
}

   empty context的作用作者下面会阐述。 

2.2 cancle context

// A cancelCtx can be canceled. When canceled, it also cancels any children
// that implement canceler.
type cancelCtx struct {
    Context

    mu       sync.Mutex            // protects following fields
    done     chan struct{}         // created lazily, closed by first cancel call
    children map[canceler]struct{} // set to nil by the first cancel call
    err      error                 // set to non-nil by the first cancel call
}

func (c *cancelCtx) Done() <-chan struct{} {
    c.mu.Lock()
    if c.done == nil {
        c.done = make(chan struct{})
    }
    d := c.done
    c.mu.Unlock()
    return d
}

func (c *cancelCtx) Err() error {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.err
}

func (c *cancelCtx) String() string {
    return fmt.Sprintf("%v.WithCancel", c.Context)
}

// cancel closes c.done, cancels each of c's children, and, if
// removeFromParent is true, removes c from its parent's children.
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
    if err == nil {
        panic("context: internal error: missing cancel error")
    }
    c.mu.Lock()
    if c.err != nil {
        c.mu.Unlock()
        return // already canceled
    }
    c.err = err
    if c.done == nil {
        c.done = closedchan
    } else {
        close(c.done)
    }
    for child := range c.children {
        // NOTE: acquiring the child's lock while holding parent's lock.
        child.cancel(false, err)
    }
    c.children = nil
    c.mu.Unlock()

    if removeFromParent {
        removeChild(c.Context, c)
    }
}

  cancelctx中的成员变量:

     `done chan struct{}`: 在调用Done()函数时会将该变量返回,这可以用在多goroutines之间进行状态同步;

`children map[canceler]struct{}`: 一个context可以被其他context 引用,被引用的context为parant,引用context为children,该变量包含所有的children context;

    `Context`:  关联了Context接口,实现了Done()和Err(),但是没有实现Value()和Deadline();

      cancel函数是一个受保护的函数,不能在外部进行调用。可以看到在执行这个函数的时候 done channel会被关闭掉,同时它会调用所有的children context的cancel函数,但是没有解除彼此的依赖关系。这实际上比较好理解,因为children context的生命周期是依赖与parant context的。同时它还要判断是否调用 removeChild(c.Context, c)函数将解除对parant context的引用关系。

   在context.WithCancel(parent Context) 函数中返回的就是cancelCtx;

2.3 timer context

// A timerCtx carries a timer and a deadline. It embeds a cancelCtx to
// implement Done and Err. It implements cancel by stopping its timer then
// delegating to cancelCtx.cancel.
type timerCtx struct {
    cancelCtx
    timer *time.Timer // Under cancelCtx.mu.
    deadline time.Time
}

func (c *timerCtx) Deadline() (deadline time.Time, ok bool) {
    return c.deadline, true
}

func (c *timerCtx) String() string {
    return fmt.Sprintf("%v.WithDeadline(%s [%s])", c.cancelCtx.Context, c.deadline, time.Until(c.deadline))
}

func (c *timerCtx) cancel(removeFromParent bool, err error) {
    c.cancelCtx.cancel(false, err)
    if removeFromParent {
        // Remove this timerCtx from its parent cancelCtx's children.
        removeChild(c.cancelCtx.Context, c)
    }
    c.mu.Lock()
    if c.timer != nil {
        c.timer.Stop()
        c.timer = nil
    }
    c.mu.Unlock()
}

   timer context 中的成员变量:

    `cancelCtx`: timerCtx 关联了 canelCtx类型;

    `timer`:  一个定时器,用来设置超时的时间;

    `dealine`: 一个时间类型,用来记录死亡时间;

  cancel函数 :1)它会先触发c.cancelCtx的cancel操作,但没有解除c.cancelCtx与parentCtx的依赖关系。2)判断是否去解除自身对于parentCtx的依赖, 3)停止它的timer,这个时候计时就结束了;

  它只实现了Deadline()函数,值得注意的是timeCtx和其包含的cancleCtx 是依赖于同一个parentCtx的。

  在WithDeadline(parent Context, deadline time.Time)和WithTimeout(parent Context, timeout time.Duration)函数中返回的就是 timerCtx;

2.4 value context

// A valueCtx carries a key-value pair. It implements Value for that key and
// delegates all other calls to the embedded Context.
type valueCtx struct {
    Context
    key, val interface{}
}

func (c *valueCtx) String() string {
    return fmt.Sprintf("%v.WithValue(%#v, %#v)", c.Context, c.key, c.val)
}

func (c *valueCtx) Value(key interface{}) interface{} {
    if c.key == key {
        return c.val
    }
    return c.Context.Value(key)
}

   value context成员变量:

    Context:它关联了Context接口

    key,val interface{} :两个变量形成一个key-value结构;

   它只实现了Value()函数,可以返回key对应的value,这里需要注意的是,在查询value的时候,如果当前context中没有,会向上级的context进行搜索,递归查询

  WithValue(parent Context, key, val interface{}) 函数返回的即为value context。

2.5 总结

  综上四种类型的context 总结如下:

Name

Deadline

Done

Err

Value

继承

empty

nil

cancel

-

-

Context

timer

-

-

-

canelCtx

value

-

-

-

Context

  我们可以发现除了empty以外其他三种类型都没有完全去实现Context接口定义的所有函数,如果直接实例化一个cancelCtx对象,但是没有对 Context部分进行赋值,当调用其Value和Deadline函数会崩溃,timerCtx和valueCtx也是同样的道理。请读者一定要记住以上四种类型,这样你会很容易理解下面的内容。

 三、 Context的使用

3.1 Context 常用函数

 我们在上面的介绍过程中提到了很多函数:

//创建一个Cancel contextfunc WithCancel(parent Context) (ctx Context, cancel CancelFunc)
//创建一个带有 deadline的Timer context
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
//创建一个带有超时的Timer context
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
//创建一个Value context
func WithValue(parent Context, key, val interface{}) Context

   这些函数都是在使用Context中经常用到的,我们接下来说明它们的功能。

WithCancel

介绍:

复制parentCtx,同时创建一个新的Done Channel返回Cancel函数,当以下两种情况发生时会关闭Done Channel,触发Done信号:

  1) 返回的Cancel函数被调用;

  2) parentCtx触发了Done信号;

  通常一个Context生命周期截止了(不再被需要的时候)就要立刻调用Cancel函数

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

   在newcancel函数中实例化一个cancel context对象:  

// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
    return cancelCtx{Context: parent}
}

   propagateCancel如注释所述:向上找到最近的可以被依赖的父context,将子context放入 parent的children队列;如果找不到就开一个goroutines来等待主链退出的通知。

// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
    if parent.Done() == nil {
        return // parent is never canceled
    }
    if p, ok := parentCancelCtx(parent); ok {
        p.mu.Lock()
        if p.err != nil {
            // parent has already been canceled
            child.cancel(false, p.err)
        } else {
            if p.children == nil {
                p.children = make(map[canceler]struct{})
            }
            p.children[child] = struct{}{}
        }
        p.mu.Unlock()
    } else {
        go func() {
            select {
            case <-parent.Done():
                child.cancel(false, parent.Err())
            case <-child.Done():
            }
        }()
    }
}

  parentCancelCtx : 寻找沿着parant引用向上追溯,直到发现一个cancelCtx;

// parentCancelCtx follows a chain of parent references until it finds a
// *cancelCtx. This function understands how each of the concrete types in this
// package represents its parent.
func parentCancelCtx(parent Context) (*cancelCtx, bool) {
   for {
      switch c := parent.(type) {
      case *cancelCtx:
         return c, true
      case *timerCtx:
         return &c.cancelCtx, true
      case *valueCtx:
         parent = c.Context
      default:
         return nil, false
      }
   }
}

   它还返回一个函数指针,这个函数指针实际上就是执行cancelCtx中的cancel函数--c.cancel(true, Canceled),该操作会解除和parent ctx的依赖。

   总的来说创建一个新的context就是在parant context中挂载一个 children context,也许传入的parent与新生成的ctx会挂载到同一个ctx下,也许会加入到parent contxt的children 队列中。我们要与上面的四种类型的context比较,empty context和 value context是不具备挂载children的能力的,而cancel context 和timer context 两种类型具备挂载chidren 的能力(实际上timerCtx的挂载能力是继承于canelCtx)。

  但问题来了,在创建cancel context时候需要传入一个parent 参数,那么这个parent从哪里来?这时候就需要 func Background() Context 这个函数,它返回一个作为起始点的context对象,而这个BackgroundCtx是一个empty context,这就是empty context的作用。在回想一下上面的介绍是不是很合理的构造?

WithDeadline

复制parentCtx,在创建的时候如果parentCtx已经触发Deadline,则直接返回一个cancelCtx和Cancel函数,否则会将返回一个timeCtx和Cancel函数。在发生以下情况的时候Done信号会被触发:

1)Cancel函数被调用

2)当前时间超过设置的Deadline

3)parentCtx触发了Done信号

func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
    if cur, ok := parent.Deadline(); ok && cur.Before(d) {
        // The current deadline is already sooner than the new one.
        return WithCancel(parent)
    }
    c := &timerCtx{
        cancelCtx: newCancelCtx(parent),
        deadline:  d,
    }
    propagateCancel(parent, c)
    dur := time.Until(d)
    if dur <= 0 {
        c.cancel(true, DeadlineExceeded) // deadline has already passed
        return c, func() { c.cancel(true, Canceled) }
    }
    c.mu.Lock()
    defer c.mu.Unlock()
    if c.err == nil {
        c.timer = time.AfterFunc(dur, func() {
            c.cancel(true, DeadlineExceeded)
        })
    }
    return c, func() { c.cancel(true, Canceled) }
}

WithValue

  复制parentCtx,保存传入的key-value键值对,没有Cancel函数被返回,但不代表该context没有done信号,只有在parentCtx的Done信号被触发的时候,才会发送。

func WithValue(parent Context, key, val interface{}) Context {
    if key == nil {
        panic("nil key")
    }
    if !reflect.TypeOf(key).Comparable() {
        panic("key is not comparable")
    }
    return &valueCtx{parent, key, val}
}

 3.2 、Context的应用示例

参考网址   

[1] https://godoc.org/golang.org/x/net/context

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
2年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Peter20 Peter20
3年前
mysql中like用法
like的通配符有两种%(百分号):代表零个、一个或者多个字符。\(下划线):代表一个数字或者字符。1\.name以"李"开头wherenamelike'李%'2\.name中包含"云",“云”可以在任何位置wherenamelike'%云%'3\.第二个和第三个字符是0的值wheresalarylike'\00%'4\
Stella981 Stella981
2年前
Python之time模块的时间戳、时间字符串格式化与转换
Python处理时间和时间戳的内置模块就有time,和datetime两个,本文先说time模块。关于时间戳的几个概念时间戳,根据1970年1月1日00:00:00开始按秒计算的偏移量。时间元组(struct_time),包含9个元素。 time.struct_time(tm_y
Easter79 Easter79
2年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这