Context

前言

ApplicationContext

问题引入

在列举上下文的用法之前,我们来看一个简单的示例:

协程泄露

func main()  {
    //打印已有协程数量
    fmt.Println("Start with goroutines num:", runtime.NumGoroutine())
    //新起子协程
    go Spawn()
    time.Sleep(time.Second)
    fmt.Println("Before finished, goroutines num:", runtime.NumGoroutine())
    fmt.Println("Main routines exit!")
}

func Spawn()  {
    count := 1
    for {
    	time.Sleep(100 * time.Millisecond)
    	count++
    }
}
复制代码

输出:

Start with goroutines num: 1
Before finished, goroutines num: 2
Main routines exit!
复制代码
runtime.NumGoroutine()

解决方式

管道通知退出

关于控制子协程的退出,可能有人会想到另一个做法,我们再来看另一个例子。

func main()  {
    defer fmt.Println("Main routines exit!")
    ExitBySignal()
    fmt.Println("Start with goroutines num:", runtime.NumGoroutine())
    //主动通知协程退出
    sig <- true
    fmt.Println("Before finished, goroutines num:", runtime.NumGoroutine())
}

//利用管道通知协程退出
func ListenWithSignal()  {
    count := 1
    for {
    	select {
    	//监听通知
    	case <-sig:
    		return
    	default:
    		//正常执行
    		time.Sleep(100 * time.Millisecond)
    		count++
    	}
    }
}
复制代码

输出:

Start with goroutines num: 2
Before finished, goroutines num: 1
Main routines exit!
复制代码

上面这个例子可以说相对优雅一些,在main协程里面主动通知子协程退出,不过两者之间的仍然存在依赖,假如子协程A又创建了新的协程B,这个时候通知只能到达子A,新协程B是无法感知的,因此同样可能会存在协程泄露的现象。

上下文管理

Go
官方概念

在此之前,先来复习下标准库的概念:

A Context carries a deadline, a cancellation signal, and other values across API boundaries. Context's methods may be called by multiple goroutines simultaneously.
上下文带着截止时间、cancel信号、还有在API之间提供值读写。

type Context interface {
    // 返回该上下文的截止时间,如果没有设置截至时间,第二个值返回false
    Deadline() (deadline time.Time, ok bool)
    
    // 返回一个管道,上下文结束(cancel)时该方法会执行,经常结合select块监听
    Done() <-chan struct{}
    
    // 当Done()执行时,Err()会返回一个error解释退出原因
    Err() error
    
    // 上下文值存储字典
    Value(key interface{}) interface{}
}
复制代码
context.Withcancel()cancel()cancel()Done()
// 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) {
    //返回新的cancelCtx子上下文
    c := newCancelCtx(parent)
    //将原上下文
    propagateCancel(parent, &c)
    return &c, func() { c.cancel(true, Canceled) }
}
复制代码

我们继续引入一个协程泄漏的demo,来看下具体使用例子:
使用示例:

//阻塞两个子协程,预期只有一个协程会正常退出
func LeakSomeRoutine() int {
    ch := make(chan int)
    //起3个协程抢着输入到ch
    go func() {
    	ch <- 1
    }()
    
    go func() {
    	ch <- 2
    }()
    
    go func() {
    	ch <- 3
    }()
    //一有输入立刻返回
    return <-ch
}

func main() {
    //每一层循环泄漏两个协程
    for i := 0; i < 4; i++ {
    	LeakSomeRoutine()
    	fmt.Printf("#Goroutines in roop end: %d.\n", runtime.NumGoroutine())
    }
}
复制代码

程序输出:

#Goroutines in roop end: 3.
#Goroutines in roop end: 5.
#Goroutines in roop end: 7.
#Goroutines in roop end: 9.
复制代码

可以看到,随着循环次数增加,除去main协程,每一轮都泄漏两个协程,所以程序退出之前最终有9个协程。

接下来我们引入上下文的概念,来管理子协程:

func FixLeakingByContex() {
    //创建上下文用于管理子协程
    ctx, cancel := context.WithCancel(context.Background())
    
    //结束前清理未结束协程
    defer cancel()
    
    ch := make(chan int)
    go CancelByContext(ctx, ch)
    go CancelByContext(ctx, ch)
    go CancelByContext(ctx, ch)
    
    // 随机触发某个子协程退出
    ch <- 1
}

func CancelByContext(ctx context.Context, ch chan (int)) int {
    select {
    case <-ctx.Done():
    	//fmt.Println("cancel by ctx.")
    	return 0
    case n := <-ch :
    	return n
    }
}

func main() {
    //每一层循环泄漏两个协程
    for i := 0; i < 4; i++ {
    	FixLeakingByContex()
    	//给它点时间 异步清理协程
    	time.Sleep(100)
    	fmt.Printf("#Goroutines in roop end: %d.\n", runtime.NumGoroutine())
    }
}
复制代码
CancelByContext
  • 上下文通知结束
  • 收到管道传入的值
FixLeakingByContex()cancel()

程序输出:

#Goroutines in roop end: 1.
#Goroutines in roop end: 1.
#Goroutines in roop end: 1.
#Goroutines in roop end: 1.
复制代码

截止退出

WithDeadline(parent Context, d time.Time)WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)Done()

使用示例:
我们摘取官网一个demo:

func main() {
    // 由于传入时间为50微妙,因此在select选择块中,ctx.Done()分支会先执行
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()
    
    select {
    case <-time.After(1 * time.Second):
    	fmt.Println("overslept")
    case <-ctx.Done():
    	fmt.Println(ctx.Err()) // prints "context deadline exceeded"
    }
}
//输出: context deadline exceeded
复制代码

请求超时

WithTimeout()http

程序示例:

func TestTimeReqWithContext(t *testing.T) {
    //初始化http请求
    request, e := http.NewRequest("GET", "https://www.pixelpigpigpig.xyz", nil)
    if e != nil {
    	log.Println("Error: ", e)
    	return
    }
    
    //使用Request生成子上下文, 并且设置截止时间为10毫秒
    ctx, cancelFunc := context.WithTimeout(request.Context(), 10*time.Millisecond)
    defer cancelFunc()
    
    //绑定超时上下文到这个请求
    request = request.WithContext(ctx)
    
    //time.Sleep(20 * time.Millisecond)
    
    //发起请求
    response, e := http.DefaultClient.Do(request)
    if e != nil {
    	log.Println("Error: ", e)
    	return
    }
    
    defer response.Body.Close()
    //如果请求没问题, 打印body到控制台
    io.Copy(os.Stdout, response.Body)
}
复制代码

我们给请求限时10毫秒,执行可以看到程序打印上下文已经过期了。
输出示例:

=== RUN   TestTimeReqWithContext
2020/05/16 23:17:14 Error:  Get https://www.pixelpigpigpig.xyz: context deadline exceeded
复制代码

Context值读写

context
// 上下文值存储字典
Value(key interface{}) interface{}
复制代码

关于它的用法,之前在“聊一聊httpRouter”的文章中列举过,可以这样子使用,

示例:

// 获取顶级上下文
ctx := context.Background()
// 在上下文写入string值, 注意需要返回新的value上下文
valueCtx := context.WithValue(ctx, "hello", "pixel")
value := valueCtx.Value("hello")
if value != nil {
    fmt.Printf("Params type: %v, value: %v.\n", reflect.TypeOf(value), value)
}
复制代码
context.Background()context

Context的作用域

ContextgocontextapplicationContextSpringgoGocontext
context.Context
GinRequestResponse

另外在参考链接处有一篇比较有意思的争论,有个作者吐槽了关于Go上下文的泛滥,关于上下文的辩论在该文章的评论可谓见仁见智。

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

总的来说,Go上下文应该着重用于管理子协程或者是有目地嵌入函数链的生命周期中,而不是约定俗成在每个方法都传播上下文。

参考链接

Goroutine leak
medium.com/golangspec/…
Understanding the context package
medium.com/rungo/under…
Context Package Semantics In Go
www.ardanlabs.com/blog/2019/0…
Context should go away for Go 2(“关于Go上下文泛滥的吐槽”)
faiface.github.io/post/contex…
Go: Context and Cancellation by Propagation
medium.com/a-journey-w…
图片ref:(Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French. )