🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
在 Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。 当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。 在Google 内部,我们开发了 Context 包,专门用来简化 对于处理单个请求的多个 goroutine 之间与请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及多个 API 调用。 context的数据结构是: ~~~go // A Context carries a deadline, cancelation signal, and request-scoped values // across API boundaries. Its methods are safe for simultaneous use by multiple // goroutines. type Context interface { // Done returns a channel that is closed when this `Context` is canceled // or times out. Done() <-chan struct{} // Err indicates why this Context was canceled, after the Done channel // is closed. Err() error // Deadline returns the time when this Context will be canceled, if any. Deadline() (deadline time.Time, ok bool) // Value returns the value associated with key or nil if none. Value(key interface{}) interface{} } ~~~ Context中的方法: * Done会返回一个channel,当该context被取消的时候,该channel会被关闭,同时对应的使用该context的routine也应该结束并返回。 * Context中的方法是协程安全的,这也就代表了在父routine中创建的context,可以传递给任意数量的routine并让他们同时访问。 * Deadline会返回一个超时时间,routine获得了超时时间后,可以对某些io操作设定超时时间。 * Value可以让routine共享一些数据,当然获得数据是协程安全的。 这里需要注意一点的是在goroutine中使用context包的时候,通常我们需要在goroutine中新创建一个上下文的context,原因是:如果直接传递外部context到协层中,一个请求可能在主函数中已经结束,在goroutine中如果还没有结束的话,会直接导致goroutine中的运行的被取消. ~~~go go func() { _, ctx, _ := log.FromContextOrNew(context.Background(), nil) }() ~~~ context.Background函数的返回值是一个空的context,经常作为树的根结点,它一般由接收请求的第一个routine创建,不能被取消、没有值、也没有过期时间。 Background函数的声明如下: ~~~go // Background returns an empty Context. It is never canceled, has no deadline, // and has no values. Background is typically used in main, init, and tests, // and as the top-level `Context` for incoming requests. func Background() Context ~~~ WithCancel 和 WithTimeout 函数 会返回继承的 Context 对象, 这些对象可以比它们的父 Context 更早地取消。 当请求处理函数返回时,与该请求关联的 Context 会被取消。 当使用多个副本发送请求时,可以使用 WithCancel取消多余的请求。 WithTimeout 在设置对后端服务器请求截止时间时非常有用。 下面是这三个函数的声明: ~~~go // WithCancel returns a copy of parent whose Done channel is closed as soon as // parent.Done is closed or cancel is called. func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // A CancelFunc cancels a Context. type CancelFunc func() // WithTimeout returns a copy of parent whose Done channel is closed as soon as // parent.Done is closed, cancel is called, or timeout elapses. The new // Context's Deadline is the sooner of now+timeout and the parent's deadline, if // any. If the timer is still running, the cancel function releases its // resources. func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) ~~~ 调用CancelFunc对象将撤销对应的Context对象,这样父结点的所在的环境中,获得了撤销子节点context的权利,当触发某些条件时,可以调用CancelFunc对象来终止子结点树的所有routine。在子节点的routine中,需要判断何时退出routine: ~~~go select { case <-cxt.Done(): // do some cleaning and return } ~~~ 根据cxt.Done()判断是否结束。当顶层的Request请求处理结束,或者外部取消了这次请求,就可以cancel掉顶层context,从而使整个请求的routine树得以退出。 WithDeadline和WithTimeout比WithCancel多了一个时间参数,它指示context存活的最长时间。如果超过了过期时间,会自动撤销它的子context。所以context的生命期是由父context的routine和deadline共同决定的。 WithValue 函数能够将请求作用域的数据与 Context 对象建立关系。声明如下: ~~~go type valueCtx struct { Context key, val interface{} } func WithValue(parent Context, key, val interface{}) Context { if key == nil { panic("nil key") } ...... return &valueCtx{parent, key, val} } func (c *valueCtx) Value(key interface{}) interface{} { if c.key == key { return c.val } return c.Context.Value(key) } ~~~ WithValue返回parent的一个副本,该副本保存了传入的`key/value`,而调用Context接口的Value(key)方法就可以得到val。注意在同一个context中设置`key/value`,若key相同,值会被覆盖。 Context上下文数据的存储就像一个树,每个结点只存储一个`key/value`对。WithValue()保存一个`key/value`对,它将父context嵌入到新的子context,并在节点中保存了`key/value`数据。Value()查询key对应的value数据,会从当前context中查询,如果查不到,会递归查询父context中的数据。 值得注意的是,context中的上下文数据并不是全局的,它只查询本节点及父节点们的数据,不能查询兄弟节点的数据。 Context 使用原则: * 不要把Context放在结构体中,要以参数的方式传递。 * 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。 * 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO。 * Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递。 * Context是线程安全的,可以放心的在多个goroutine中传递。