Golang context深入理解
package Context里的说明
golang有个context的包,它定义了一个Context 类型,该类型在API边界之间以及进程之间传递截止日期,取消信号和其他请求范围的值。
传入服务器的请求应创建一个Context,传出调用到服务器的就接受一个Context(Incoming requests to a server should create a Context, and outgoing calls to servers should accept a Context.)。在函数调用链上,函数必须传递这个Context,或使用 WithCancel, WithDeadline, WithTimeout, or WithValue创建一个派生的Context替代它。当一个Context取消,所有派生自它的Contexts也会被取消。
WithCancel, WithDeadline, and WithTimeout 函数以一个父Context作为参数,返回一个派生的Context(子Context)和CancelFunc取消函数。调用CancelFunc取消这个子Context和它的 孩子Context,删除父Context的引用,和停止相关时钟。未能调用CancelFunc会使子代及其子代泄漏,直到父代被取消或计时器触发。go vet tool 检查所有控制流路径上是否都使用了CancelFuncs。
使用Context的程序应遵循以下规则,以使各个包之间的接口保持一致,并使静态分析工具可以检查上下文传播:
不要将Context存储在结构类型中;而是将Context明确传递给需要它的每个函数。Context应该是第一个参数,通常命名为ctx:
func DoSomething(ctx context.Context, arg Arg) error {
// ... use ctx ...
}
不要传递一个nil Context,即便函数允许。如果不知道传递哪个Context就用context.TODO。
context Values仅用作传递进程和API的请求范围数据,而不用于将可选参数传递给函数。(Use context Values only for request-scoped data that transits processes and APIs, not for passing optional parameters to functions.)
同样的一个Context可以传递给运行在不同的goroutines的函数,Context对于由多个goroutine同时使用是安全的。
Context应该有哪里用?
按我目前的理解是,在有请求进入服务器时,这时可以创建个Context,然后在其它需要异步执行的操作中,例如读写数据库、Redis、IO等,在发读写请求时传入Context。在超时或客户端已断开链接时,可以让还没完成任务的goroutine提前退出,节省服务器资源。
创建Context
package context里的函数和方法
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context
context.Background() ctx Context
返回一个empty context,它用于最顶级的地方(main或者请求handler里的开始处)。它可以生成子Context。
context.TODO() ctx Context
也返回一个empty context,在最顶级或你不知道用哪个Context时,或在实现某功能时还没有具体的Context时用。
在代码里,这个TODO的实现和上面的Background是一样的,区别在于,静态分析工具可以使用它来验证Context是否正确传递,这是一个重要的细节,因为静态分析工具可以帮助及早发现潜在的错误,并且可以将其连接到CI / CD管道中。
下面四个通过派生的方式创建context:
func WithValue(parent Context, key, val interface{}) Context
从parent Context派生出一个子Context,里面包含key val的键值对,键值对是随着context树传递下去。这意味着一旦获得带有值的Context,从此派生的任何Context都将获得该值。不建议使用Context值来传递关键参数,相反是在函数中明确使用对应参数。
context.WithCancel(parent Context) (ctx Context, cancel CancelFunc)
这里有个CancelFunc函数,正常是那个函数调用WithCancel创建的,由它来调用CancelFunc。不要将这个CancelFunc作为参数来传递。
context.WithDeadline(parent Context, d time.Time) (ctx Context, cancel CancelFunc)
超时和调用Cancel时可取消。
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(2 * time.Second))
context.WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)
与WithDeadline相似,只是第二个参数不一样。
在函数中接受和使用Context
下面的例子中,在一个函数里接收一个Context并启动一个goroutine,同时等待goroutine返回或context取消。select语句帮助我们选择最先出现的情况。
总结:如下例子所示,接收Context的函数开始一个新goroutine后,通过select case <-ctx.Done(): 和case goroutine是否完成(通常是一个数据channel,看数据是否回来)来判断怎么退出函数。
例子:
package main
import (
"context"
"fmt"
"math/rand"
"time"
)
//Slow function
func sleepRandom(fromFunction string, ch chan int) {
//defer cleanup
defer func() { fmt.Println(fromFunction, "sleepRandom complete") }()
//Perform a slow task
//For illustration purpose,
//Sleep here for random ms
seed := time.Now().UnixNano()
r := rand.New(rand.NewSource(seed))
randomNumber := r.Intn(100)
sleeptime := randomNumber + 100
fmt.Println(fromFunction, "Starting sleep for", sleeptime, "ms")
time.Sleep(time.Duration(sleeptime) * time.Millisecond)
fmt.Println(fromFunction, "Waking up, slept for ", sleeptime, "ms")
//write on the channel if it was passed in
if ch != nil {
ch <- sleeptime
}
}
//Function that does slow processing with a context
//Note that context is the first argument
func sleepRandomContext(ctx context.Context, ch chan bool) {
//Cleanup tasks
//There are no contexts being created here
//Hence, no canceling needed
defer func() {
fmt.Println("sleepRandomContext complete")
ch <- true
}()
//Make a channel
sleeptimeChan := make(chan int)
//Start slow processing in a goroutine
//Send a channel for communication
go sleepRandom("sleepRandomContext", sleeptimeChan)
//Use a select statement to exit out if context expires
select {
case <-ctx.Done():
//If context is cancelled, this case is selected
//This can happen if the timeout doWorkContext expires or
//doWorkContext calls cancelFunction or main calls cancelFunction
//Free up resources that may no longer be needed because of aborting the work
//Signal all the goroutines that should stop work (use channels)
//Usually, you would send something on channel,
//wait for goroutines to exit and then return
//Or, use wait groups instead of channels for synchronization
fmt.Println("sleepRandomContext: Time to return")
case sleeptime := <-sleeptimeChan:
//This case is selected when processing finishes before the context is cancelled
fmt.Println("Slept for ", sleeptime, "ms")
}
}
//A helper function, this can, in the real world do various things.
//In this example, it is just calling one function.
//Here, this could have just lived in main
func doWorkContext(ctx context.Context) {
//Derive a timeout context from context with cancel
//Timeout in 150 ms
//All the contexts derived from this will returns in 150 ms
ctxWithTimeout, cancelFunction := context.WithTimeout(ctx, time.Duration(150)*time.Millisecond)
//Cancel to release resources once the function is complete
defer func() {
fmt.Println("doWorkContext complete")
cancelFunction()
}()
//Make channel and call context function
//Can use wait groups as well for this particular case
//As we do not use the return value sent on channel
ch := make(chan bool)
go sleepRandomContext(ctxWithTimeout, ch)
//Use a select statement to exit out if context expires
select {
case <-ctx.Done():
//This case is selected when the passed in context notifies to stop work
//In this example, it will be notified when main calls cancelFunction
fmt.Println("doWorkContext: Time to return")
case <-ch:
//This case is selected when processing finishes before the context is cancelled
fmt.Println("sleepRandomContext returned")
}
}
func main() {
//Make a background context
ctx := context.Background()
//Derive a context with cancel
ctxWithCancel, cancelFunction := context.WithCancel(ctx)
//defer canceling so that all the resources are freed up
//For this and the derived contexts
defer func() {
fmt.Println("Main Defer: canceling context")
cancelFunction()
}()
//Cancel context after a random time
//This cancels the request after a random timeout
//If this happens, all the contexts derived from this should return
go func() {
sleepRandom("Main", nil)
cancelFunction()
fmt.Println("Main Sleep complete. canceling context")
}()
//Do work
doWorkContext(ctxWithCancel)
}
参考:
https://golang.org/pkg/context/
http://p.agnihotry.com/post/understanding_the_context_package_in_golang/