文章目录
- 1.什么是 Context
- 2.为什么要有 Context
- 3.context 包源码一览
- 3.1 Context
- 3.2 CancelFunc
- 3.3 canceler
- 3.4 Context 的实现
- 3.4.1 emptyCtx
- 3.4.2 cancelCtx
- 3.4.3 timerCtx
- 3.4.4 valueCtx
- 4.Context 的用法
- 4.1 使用建议
- 4.2 传递共享的数据
- 4.3 取消 goroutine
- 4.4 防止 goroutine 泄漏
- 5.Context 的不足
- 6.小结
- 参考文献
Go 1.7 标准库引入 Context,中文名为上下文,是一个跨 API 和进程用来传递截止日期、取消信号和请求相关值的接口。
context.Context 定义如下:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline()
Done()
Err()context.Canceledcontext.DeadlineExceeded
Value()
另外,context 包中提供了两个创建默认上下文的函数:
// TODO 返回一个非 nil 但空的上下文。
// 当不清楚要使用哪种上下文或无可用上下文尚应使用 context.TODO。
func TODO() Context
// Background 返回一个非 nil 但空的上下文。
// 它不会被 cancel,没有值,也没有截止时间。它通常由 main 函数、初始化和测试使用,并作为处理请求的顶级上下文。
func Background() Context
还有四个基于父级创建不同类型上下文的函数:
// WithCancel 基于父级创建一个具有 Done channel 的 context
func WithCancel(parent Context) (Context, CancelFunc)
// WithDeadline 基于父级创建一个不晚于 d 结束的 context
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
// WithTimeout 等同于 WithDeadline(parent, time.Now().Add(timeout))
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
// WithValue 基于父级创建一个包含指定 key 和 value 的 context
func WithValue(parent Context, key, val interface{}) Context
在后面会详细介绍这些不同类型 context 的用法。
2.为什么要有 ContextGo 为后台服务而生,如只需几行代码,便可以搭建一个 HTTP 服务。
在 Go 的服务里,通常每来一个请求都会启动若干个 goroutine 同时工作:有些执行业务逻辑,有些去数据库拿数据,有些调用下游接口获取相关数据…
协程 a 生 b c d,c 生 e,e 生 f。父协程与子孙协程之间是关联在一起的,他们需要共享请求的相关信息,比如用户登录态,请求超时时间等。如何将这些协程联系在一起,context 应运而生。
话说回来,为什么要将这些协程关联在一起呢?以超时为例,当请求被取消或是处理时间太长,这有可能是使用者关闭了浏览器或是已经超过了请求方规定的超时时间,请求方直接放弃了这次请求结果。此时所有正在为这个请求工作的 goroutine 都需要快速退出,因为它们的“工作成果”不再被需要了。在相关联的 goroutine 都退出后,系统就可以回收相关资源了。
总的来说 context 的作用是为了在一组 goroutine 间传递上下文信息(cancel signal,deadline,request-scoped value)以达到对它们的管理控制。
3.context 包源码一览我们分析的 Go 版本依然是 1.17。
3.1 Context
context 是一个接口,某个类型只要实现了其申明的所有方法,便实现了 context。再次看下 context 的定义。
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
方法的作用在前文已经详述,这里不再赘述。
3.2 CancelFunc
另外 context 包中还定义了一个函数类型 CancelFunc,
type CancelFunc func()
CancelFunc 通知操作放弃其工作。CancelFunc 不会等待工作停止。多个 goroutine 可以同时调用 CancelFunc。在第一次调用之后,对 CancelFunc 的后续调用不会执行任何操作。
3.3 canceler
context 包还定义了一个更加简单的用于取消操作的 context,名为 canceler,其定义如下。
// A canceler is a context type that can be canceled directly. The
// implementations are *cancelCtx and *timerCtx.
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}
*cancelCtx*timerCtx
cancel()Done()
(1)“取消”操作应该是建议性,而非强制性。 caller 不应该去关心、干涉 callee 的情况,决定如何以及何时 return 是 callee 的责任。caller 只需发送“取消”信息,callee 根据收到的信息来做进一步的决策,因此接口并没有定义 cancel 方法。
(2)“取消”操作应该可传递。 “取消”某个函数时,和它相关联的其他函数也应该“取消”。因此,Done() 方法返回一个只读的 channel,所有相关函数监听此 channel。一旦 channel 关闭,通过 channel 的“广播机制”,所有监听者都能收到。
3.4 Context 的实现
context 包中定义了 Context 接口后,并且给出了四个实现,分别是:
- emptyCtx
- cancelCtx
- timerCtx
- valueCtx
我们可以根据不同场景选择使用不同的 Context。
3.4.1 emptyCtx
emptyCtx 正如其名,是一个空上下文。无法被取消,不携带值,也没有截止日期。
// 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
}
其未被导出,但被包装成如下两个变量,通过相应的导出函数对外提供使用。
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
}
// TODO returns a non-nil, empty Context. Code should use context.TODO when
// it's unclear which Context to use or it is not yet available (because the
// surrounding function has not yet been extended to accept a Context
// parameter).
func TODO() Context {
return todo
}
Background()TODO()
Background()TODO()
3.4.2 cancelCtx
cancelCtx 是一个用于取消操作的 Context,实现了 canceler 接口。它直接将接口 Context 作为它的一个匿名字段,这样,它就可以被看成一个 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 atomic.Value // of 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
}
WithCancel()
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
传入一个父 Context(这通常是一个 background,作为根结点),返回新建的 Context,新 Context 的 done channel 是新建的。
WithCancel()
Done()
func (c *cancelCtx) Done() <-chan struct{} {
d := c.done.Load()
if d != nil {
return d.(chan struct{})
}
c.mu.Lock()
defer c.mu.Unlock()
d = c.done.Load()
if d == nil {
d = make(chan struct{})
c.done.Store(d)
}
return d.(chan struct{})
}
Done()
Err()String()Err()String()
func (c *cancelCtx) Err() error {
c.mu.Lock()
err := c.err
c.mu.Unlock()
return err
}
type stringer interface {
String() string
}
func contextName(c Context) string {
if s, ok := c.(stringer); ok {
return s.String()
}
return reflectlite.TypeOf(c).String()
}
func (c *cancelCtx) String() string {
return contextName(c.Context) + ".WithCancel"
}
cancel()
// 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
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
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)
}
}
cancel()
当 WithCancel() 函数返回的 CancelFunc 被调用或者父结点的 done channel 被关闭(父结点的 CancelFunc 被调用),此 context(子结点) 的 done channel 也会被关闭。
cancel()
var Canceled = errors.New("context canceled")
还要注意到一点,调用子结点 cancel 方法的时候,传入的第一个参数 removeFromParent 是 false。
removeFromParent 什么时候会传 true,什么时候传 false 呢?
先看一下当 removeFromParent 为 true 时,会将当前 context 从父结点中删除操作。
// removeChild removes a context from its parent.
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()
}
delete(p.children, child)
什么时候会传 true 呢?答案是调用 WithCancel() 方法的时候,也就是新创建一个用于取消的 context 结点时,返回的 cancelFunc 函数会传入 true。这样做的结果是:当调用返回的 cancelFunc 时,会将这个 context 从它的父结点里“除名”,因为父结点可能有很多子结点,我自己取消了,需要清理自己,从父亲结点删除自己。
cancel()c.children = nilcancel()
如上左图,代表一棵 Context 树。当调用左图中标红 Context 的 cancel 方法后,该 Context 从它的父 Context 中去除掉了:实线箭头变成了虚线。且虚线圈框出来的 Context 都被取消了,圈内的 context 间的父子关系都荡然无存了。
WithCancel()propagateCancel(parent, &c)
// propagateCancel arranges for child to be canceled when parent is.
func propagateCancel(parent Context, child canceler) {
done := parent.Done()
if done == nil {
return // parent is never canceled
}
select {
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
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)
} else {
if p.children == nil {
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}
该函数的作用就是将生成的当前 cancelCtx 挂靠到“可取消”的父 Context,这样便形成了上面描述的 Context 树,当父 Context 被取消时,能够将取消操作传递至子 Context。
这里着重解释下为什么会有 else 描述的情况发生。else 是指当前结点 Context 没有向上找到可以取消的父结点,那么就要再启动一个协程监控父结点或者子结点的取消动作。
case <-parent.Done()case <-child.Done()
parentCancelCtx()
// parentCancelCtx returns the underlying *cancelCtx for parent.
// It does this by looking up parent.Value(&cancelCtxKey) to find
// the innermost enclosing *cancelCtx and then checking whether
// parent.Done() matches that *cancelCtx. (If not, the *cancelCtx
// has been wrapped in a custom implementation providing a
// different done channel, in which case we should not bypass it.)
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(&cancelCtxKey)*struct*cancelCtx
3.4.3 timerCtx
timerCtx 是一个可以被取消的计时器上下文,基于 cancelCtx,只是多了一个 time.Timer 和一个 deadline。Timer 会在 deadline 到来时,自动取消 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
}
cancel()
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()
}
WithTimeout()
// WithTimeout returns WithDeadline(parent, time.Now().Add(timeout)).
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete:
//
// func slowOperationWithTimeout(ctx context.Context) (Result, error) {
// ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
// defer cancel() // releases resources if slowOperation completes before timeout elapses
// return slowOperation(ctx)
// }
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
WithDeadline()WithDeadline()
// WithDeadline returns a copy of the parent context with the deadline adjusted
// to be no later than d. If the parent's deadline is already earlier than d,
// WithDeadline(parent, d) is semantically equivalent to parent. The returned
// context's Done channel is closed when the deadline expires, when the returned
// cancel function is called, or when the parent context's Done channel is
// closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
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(false, 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) }
}
也就是说仍然要把子节点挂靠到父结点,一旦父结点取消了,会把取消信号向下传递到子结点,子结点随之取消。
有一个特殊情况是,如果要创建的这个子结点的 deadline 比父结点要晚,也就是说如果父结点是时间到自动取消,那么一定会取消这个子结点,导致子结点的 deadline 根本不起作用,因为子结点在 deadline 到来之前就已经被父结点取消了。
这个函数最核心的一句是:
c.timer = time.AfterFunc(d, func() {
c.cancel(true, DeadlineExceeded)
})
DeadlineExceeded
// DeadlineExceeded is the error returned by Context.Err when the context's
// deadline passes.
var DeadlineExceeded error = deadlineExceededError{}
type deadlineExceededError struct{}
func (deadlineExceededError) Error() string { return "context deadline exceeded" }
func (deadlineExceededError) Timeout() bool { return true }
func (deadlineExceededError) Temporary() bool { return true }
3.4.4 valueCtx
valueCtx 是一个只用于传值的 Context,其携带一个键值对,其他的功能则委托给内嵌的 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 contextName(c.Context) + ".WithValue(type " +
reflectlite.TypeOf(c.key).String() +
", val " + stringify(c.val) + ")"
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
由于它直接将 Context 作为匿名字段,因此仅管它只实现了 2 个方法,其他方法继承自父 Context。但它仍然是一个 Context,这是 Go 语言的一个特点。
WithValue()
// WithValue returns a copy of parent in which the value associated with key is
// val.
//
// Use context Values only for request-scoped data that transits processes and
// APIs, not for passing optional parameters to functions.
//
// The provided key must be comparable and should not be of type
// string or any other built-in type to avoid collisions between
// packages using context. Users of WithValue should define their own
// types for keys. To avoid allocating when assigning to an
// interface{}, context keys often have concrete type
// struct{}. Alternatively, exported context key variables' static
// type should be a pointer or interface.
func WithValue(parent Context, key, val interface{}) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
return &valueCtx{parent, key, val}
}
对 key 的要求是可比较,因为之后需要通过 key 取出 context 中的值,可比较是必须的。
通过层层传递 context,最终形成这样一棵树:
WithValue()
Value()
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
因为查找方向是往上走的,所以,父结点没法获取子结点存储的值,子结点却可以获取父结点的值。
WithValue 创建 Context 结点的过程实际上就是创建链表节点的过程。两个结点的 key 值是可以相等的,但它们是两个不同的 Context 结点。查找的时候,会向上查找到最后一个挂载的 Context 结点,也就是离得比较近的一个父结点 Context。所以,整体上而言,用 WithValue 构造的其实是一个低效率的链表。
如果你接手过项目,肯定经历过这样的窘境:在一个处理过程中,有若干子函数、子协程。各种不同的地方会向 context 里塞入各种不同的 k-v 对,最后在某个地方使用。
你根本就不知道什么时候什么地方传了什么值?这些值会不会被“覆盖”(底层是两个不同的 Context 节点,查找的时候,只会返回一个结果)?你肯定会崩溃的。
而这也是 Context 最受争议的地方,很多人建议尽量不要通过 Context 传值。
4.Context 的用法4.1 使用建议
Background()
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
官方 context 包说明文档中已经给出了 context 的使用建议:
1.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.
2.Do not pass a nil Context, even if a function permits it. Pass context.TODO if you are unsure about which Context to use.
3.Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.
4.The same Context may be passed to functions running in different goroutines; Contexts are safe for simultaneous use by multiple goroutines.
对应的中文释义为: 1.不要将 Context 塞到结构体里;直接将 Context 类型作为函数的第一参数,且命名为 ctx。
2.不要向函数传入一个 nil Context,如果你实在不知道传哪个 Context 请传 context.TODO。
3.不要把本应该作为函数参数的数据放到 Context 中传给函数,Context 只存储请求范围内在不同进程和 API 间共享的数据(如登录信息 Cookie)。
4.同一个 context 可能会被传递到多个 goroutine,别担心,context 是并发安全的。
4.2 传递共享的数据
对于 Web 服务端开发,往往希望将一个请求处理的整个过程串起来,这就非常依赖于 Thread Local(对于 Go 可理解为单个协程所独有) 的变量,而在 Go 语言中并没有这个概念,因此需要在函数调用的时候传递 Context。
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
process(ctx)
ctx = context.WithValue(ctx, "traceID", "foo")
process(ctx)
}
func process(ctx context.Context) {
traceId, ok := ctx.Value("traceID").(string)
if ok {
fmt.Printf("process over. trace_id=%s\n", traceId)
} else {
fmt.Printf("process over. no trace_id\n")
}
}
运行输出:
process over. no trace_id
process over. trace_id=foo
当然,现实场景中可能是从一个 HTTP 请求中获取到 Request-ID。所以,下面这个样例可能更适合:
const requestIDKey int = 0
func WithRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(
func(rw http.ResponseWriter, req *http.Request) {
// 从 header 中提取 request-id
reqID := req.Header.Get("X-Request-ID")
// 创建 valueCtx。使用自定义的类型,不容易冲突
ctx := context.WithValue(
req.Context(), requestIDKey, reqID)
// 创建新的请求
req = req.WithContext(ctx)
// 调用 HTTP 处理函数
next.ServeHTTP(rw, req)
}
)
}
// 获取 request-id
func GetRequestID(ctx context.Context) string {
ctx.Value(requestIDKey).(string)
}
func Handle(rw http.ResponseWriter, req *http.Request) {
// 拿到 reqId,后面可以记录日志等等
reqID := GetRequestID(req.Context())
...
}
func main() {
handler := WithRequestID(http.HandlerFunc(Handle))
http.ListenAndServe("/", handler)
}
4.3 取消 goroutine
Context 的作用是为了在一组 goroutine 间传递上下文信息,其重便包括取消信号。取消信号可用于通知相关的 goroutine 终止执行,避免无效操作。
我们先来设想一个场景:打开外卖的订单页,地图上显示外卖小哥的位置,而且是每秒更新 1 次。app 端向后台发起 websocket 连接(现实中可能是轮询)请求后,后台启动一个协程,每隔 1 秒计算 1 次小哥的位置,并发送给端。如果用户退出此页面,则后台需要“取消”此过程,退出 goroutine,系统回收资源。
后端可能的实现如下:
func Perform() {
for {
calculatePos()
sendResult()
time.Sleep(time.Second)
}
}
如果需要实现“取消”功能,并且在不了解 Context 功能的前提下,可能会这样做:给函数增加一个指针型的 bool 变量,在 for 语句的开始处判断 bool 变量是发由 true 变为 false,如果改变,则退出循环。
上面给出的简单做法,可以实现想要的效果。没有问题,但是并不优雅。并且一旦通知的信息多了之后,函数入参就会很臃肿复杂。优雅的做法,自然就要用到 Context。
func Perform(ctx context.Context) {
for {
calculatePos()
sendResult()
select {
case <-ctx.Done():
// 被取消,直接返回
return
case <-time.After(time.Second):
// block 1 秒钟
}
}
}
主流程可能是这样的:
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
go Perform(ctx)
// ……
// app 端返回页面,调用cancel 函数
cancel()
注意一个细节,WithTimeut 函数返回的 Context 和 cancelFun 是分开的。Context 本身并没有取消函数,这样做的原因是取消函数只能由外层函数调用,防止子结点 Context 调用取消函数,从而严格控制信息的流向:由父结点 Context 流向子结点 Context。
4.4 防止 goroutine 泄漏
前面那个例子里,goroutine 还是会自己执行完,最后返回,只不过会多浪费一些系统资源。这里给出一个如果不用 context 取消,goroutine 就会泄漏的例子(源自Using contexts to avoid leaking goroutines)。
// gen 是一个整数生成器且会泄漏 goroutine
func gen() <-chan int {
ch := make(chan int)
go func() {
var n int
for {
ch <- n
n++
time.Sleep(time.Second)
}
}()
return ch
}
上面的生成器会启动一个具有无限循环的 goroutine,调用者会从信道这些值,直到 n 等于 5。
for n := range gen() {
fmt.Println(n)
if n == 5 {
break
}
}
当 n == 5 的时候,直接 break 掉。那么 gen 函数的协程就会无限循环,永远不会停下来。发生了 goroutine 泄漏。
我们可以使用 Context 主动通知 gen 函数的协程停止执行,阻止泄漏。
func gen(ctx context.Context) <-chan int {
ch := make(chan int)
go func() {
var n int
for {
select {
case <-ctx.Done():
return // 当 ctx 结束时避免 goroutine 泄漏
case ch <- n:
n++
}
}
}()
return ch
}
现在,调用方可以在完成后向生成器发送信号。调用 cancel 函数后,内部 goroutine 将返回。
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // make sure all paths cancel the context to avoid context leak
for n := range gen(ctx) {
fmt.Println(n)
if n == 5 {
cancel()
break
}
}
// ...
5.Context 的不足Context 的作用很明显,当我们在开发后台服务时,能帮助我们完成对一组相关 goroutine 的控制并传递共享数据。注意是后台服务,而不是所有的场景都需要使用 Context。
Go 官方建议我们把 Context 作为函数的第一个参数,甚至连名字都准备好了。这造成一个后果:因为我们想控制所有的协程的取消动作,所以需要在几乎所有的函数里加上一个 Context 参数。很快,我们的代码里,context 将像病毒一样扩散的到处都是。
另外,像 WithCancel、WithDeadline、WithTimeout、WithValue 这些创建函数,实际上是创建了一个个的链表结点而已。我们知道,对链表的操作,通常都是 O(n) 复杂度的,效率不高。
Context 解决的核心问题是 cancelation,即便它不完美,但它却简洁地解决了这个问题。
6.小结Go 1.7 引入 context 包,目的是为了解决一组相关 goroutine 的取消问题,即并发控制。当然还可以用于传递一些共享的数据。这种场景往往在开发后台 server 时会遇到,所以 context 有其适用的场景,而非所有场景。
使用上,先创建一个根结点的 Context,之后根据 context 包提供的四个函数创建相应功能的子结点 context。由于它是并发安全的,所以可以放心地传递。
context 并不完美,有固定的使用场景,切勿滥用。