在Go http包的Server中,每一个请求都有一个对应的goroutine去处理。请求处理函数通常会启动额外的goroutine用来访问后端服务。比如数据库的RPC服务。用来处理一个请求的goroutine通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求截止时间。当一个请求被取消或者超时时,所有用来处理该请求的goroutine都应该迅速被退出,然受系统才能释放这些goroutine占用的资源。
为什么需要context 基本示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 var wg sync.WaitGroupfunc work () { for { fmt.Println("worker" ) time.Sleep(time.Second) } wg.Done() } func main () { wg.Add(1 ) go work() wg.Wait() fmt.Printf("Over" ) }
全局变量方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 var wg sync.WaitGroupvar exit bool func work () { for { fmt.Println("worker" ) time.Sleep(time.Second) if exit { break } } wg.Done() } func main () { wg.Add(1 ) go work() time.Sleep(time.Second * 3 ) exit = true wg.Wait() fmt.Printf("Over" ) }
通道方式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 func work (exitChan chan struct {}) {Loop: for { fmt.Println("worker" ) time.Sleep(time.Second) select { case <- exitChan: break Loop default : } } wg.Done() } func main () { var exitChan = make (chan struct {}, 1 ) wg.Add(1 ) go work(exitChan) time.Sleep(time.Second * 3 ) close (exitChan) wg.Wait() fmt.Printf("Over" ) }
context方式 父goroutine结束,子goroutine全都会结束
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 var wg sync.WaitGroupfunc work (ctx context.Context) { go work2(ctx) LOOP: for { fmt.Println("worker" ) time.Sleep(time.Second) select { case <-ctx.Done(): break LOOP default : } } wg.Done() } func work2 (ctx context.Context) {LOOP: for { fmt.Println("work2" ) time.Sleep(time.Second) select { case <-ctx.Done(): break LOOP default : } } } func main () { ctx, cancel := context.WithCancel(context.Background()) wg.Add(1 ) go work(ctx) time.Sleep(time.Second * 3 ) cancel() wg.Wait() fmt.Printf("Over" ) }
context初识 Go1.7加入了一个新的标准库context
,它定义了Context
类型,专门用来简化对于处理单个请求的多个goroutine之间与请求域的数据,取消信号,截至时间等相关操作,这些操作可能涉及到多个API的调用。
对于服务器传入的请求应该创建上下文,而对服务器的传出调用应该接收上下文。他们之间的函数调用链必须传递上下文,或者可以使用WithCancel
,WithDeadline
,WithTimeout
或WithValue
创建的派生上下文。当一个上下文被取消时,他派生的上下文也被取消。
Context接口 context.Context
是一个接口,该接口定义了四个需要实现的方法。
1 2 3 4 5 6 type Context interface { Deadline() (deadline time.Time, ok bool ) Done() <-chan struct {} Err() error Value(key interface {}) interface {} }
其中:
1、Deadline
方法需要返回当前Context被取消的时间,就是完成工作的截止时间(deadline)。
2、Done
方法需要返回一个Channel
,这个Channel会在当前工作完成或者上下文被取消之后关闭,多次调用Done
方法或返回同一个Channel。
3、Err
方法会返回当前Context
结束的原因,它只会在Done
返回的Channel被关闭时才会返回非空的值。
如果当前Context
被取消就返回Canceled
错误。
如果当前Context
超时就返回DeadlineExceeded
错误。
4、Value
方法会从Context
中返回键对应的值,对于同一个上下文来说,多次调用Value
并传入相同的key
会返回相同的结果,该方法仅用于传递跨API和进程间的请求域的数据。
Background()和TODO() Go内置两个函数: Background()
和TODO()
,这两个函数分别返回一个实现了Context
接口的Background
和TODO
。我们代码中最开始都是以这两个内置的上下文对象作为最顶层的partent context
,衍生出更多的子上下文对象。
Background()
主要用于main函数、初始化以及测试代码中,作为Context这个树结构的最顶层的Context,也就是根节点。
TODO()
,他目前还不知道具体的使用场景,如果我们不知道该使用什么Context时就可以使用这个。
Background()
和TODO()
本质上都是emptyCtx
结构体类型,是一个不可取消,没有设置截止时间,没有携带任何价值的Context。
With系列函数 WithCancel WithCancel
的函数签名如下:
1 func WithCancel (parent Context) (ctx Context, cancel CancelFunc)
WithCancel
返回带有Done通道的父节点的副本。当调用返回的cancel函数或当关闭父上下文的Done通道时,将关闭返回上下文的Done通道,无论什么情况。
取消此上下文将释放与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 func main () { ctx, cancel := context.WithCancel(context.Background()) defer cancel() for n := range root(ctx) { fmt.Println(n) if n == 5 { break } } } func root (ctx context.Context) <-chan int { dst := make (chan int ) n := 1 go func () { for { select { case <-ctx.Done(): return case dst <- n: n++ } } }() return dst }
root
函数在单独的goroutine中生成整数并将他们发送到返回的通道。root
的调用者在使用生成的整数之后需要取消上下文,以免root
启动的goroutine发生泄漏。
WithDeadline WithDeadline
的函数签名如下:
1 func WithDeadline (parent Context, d time.Time) (Context, CancelFunc)
返回父上下文的副本,并将deadline调整为不迟于d,如果父上下文的deadlin已经早于d,则WithDeadline(parent,d)在语义上等同于父上下文。当截止日期过时,当调用返回的cancel函数时,或当父上下文的Done通道关闭时,返回上下文的Done通道将被关闭,以最先发生的情况为准。
取消此上下文将释与其关联的资源,因此代码应该在此上下文中运行的操作完成后立即调用cancel。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func main () { d := time.Now().Add(50 * time.Millisecond) ctx, cancel := context.WithDeadline(context.Background(), d) defer cancel() select { case <-time.After(1 * time.Second): fmt.Println("overslept" ) case <- ctx.Done(): fmt.Println(ctx.Err()) } }
上面的代码中,定义了一个50毫秒之后过期的deadline,然后我们调用context.WithDeadline(context.Background(), d)
得到一个上下文ctx
和一个取消函数cancel
,然后使用一个select让主程序陷入等待:等待1秒后打印overslept
退出或者等待ctx过期后退出。因此ctx 50微秒后就过期了,所以ctx.Done
会先接收到值,上面的代码会打印ctx.Err()取消的原因。
WithTimeout WithTime
的函数签名如下:
1 func WithTimeout (parent Context, timeout time.Duration) (Context, CancelFunc)
WithTime
返回WithDeadline(parent, time.Now().Add(time))
。
取消此上下文将释放与其相关的资源,因此代码应该在此上下文中运行的操作完成过后立即调用cancel,通常用于数据库或者网络连接的超时控制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func main () { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50 ) wg.Add(1 ) go worker(ctx) time.Sleep(time.Second * 5 ) cancel() fmt.Println("Over" ) } func worker (ctx context.Context) {LOOP: for { fmt.Println("db connecting ..." ) time.Sleep(time.Millisecond * 10 ) select { case <-ctx.Done(): break LOOP default : } } fmt.Println("worker done!" ) wg.Done() }
WithValue WithValue
函数能够将请求作用域的数据与Context对象建立关系。
函数签名如下:
1 func WithValue (parent Context, key, val interface {}) Context
WithValue
返回父节点的副本,其中key关联的值为val。
仅对API和进程间传递请求域的数据使用上下文,而不是使用它来传递可选参数给函数。
所提供的键必须是可比较的,并且不应该是string
类型或者任何其它内置类型,以避免使用上下文在包之间的冲突。WithValue
的用户应该为键定义自己的类型。为了避免在分配给interface{}时进行分配,上下文键通常具有具体类型struct{}
。或者,导出的上下文关键变量的静态类型应该是指针或接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 var wg sync.WaitGrouptype TraceCode string var key = TraceCode("Trace_Code" )func main () { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*50 ) ctx = context.WithValue(ctx, key, "123456" ) wg.Add(1 ) go worker(ctx) time.Sleep(time.Second * 5 ) cancel() wg.Wait() fmt.Println("over" ) } func worker (ctx context.Context) { traceCode, ok := ctx.Value(key).(string ) if !ok { fmt.Println("invalid trace code" ) } LOOP: for { fmt.Printf("worker, trace code:%s\n" , traceCode) time.Sleep(time.Millisecond * 10 ) select { case <-ctx.Done(): break LOOP default : } } fmt.Println("worker done!" ) wg.Done() }
使用context的注意事项
1、推荐以参数的方式显示传递Context
2、给Context作为参数的函数方法,应该把Context作为第一个参数。
3、给一个函数方法传递Context的时候,不要传递nil,如果不直到要传递什么,就使用context.TODO()
4、Context的Value相关方法应该传递请求的必要数据,不应该用于传递可选参数。
5、Context是线程安全的,可以放心的在多个goroutine中的传递。
客户端超时请求示例 Server端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package mainimport ( "fmt" "math/rand" "net/http" "time" ) func main () { http.HandleFunc("/" , indexHandler) err := http.ListenAndServe(":8080" , nil ) if err != nil { panic (err) } } func indexHandler (resp http.ResponseWriter,req *http.Request) { number := rand.Intn(2 ) if number == 0 { time.Sleep(time.Second * 10 ) fmt.Fprintf(resp, "slow response" ) return } fmt.Fprintf(resp, "quick response" ) }
client 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 package mainimport ( "context" "fmt" "io/ioutil" "net/http" "sync" "time" ) type respData struct { resp *http.Response err error } func main () { ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100 ) defer cancel() doCall(ctx) } func doCall (ctx context.Context) { transport := http.Transport{DisableKeepAlives: true } client := http.Client{Transport: &transport} respChan := make (chan *respData, 1 ) request, err := http.NewRequest("GET" , "http://127.0.0.1:8080" , nil ) if err != nil { fmt.Printf("new reuest failed, err:%#v\n" , err) return } request = request.WithContext(ctx) var wg sync.WaitGroup wg.Add(1 ) defer wg.Wait() go func () { response, err := client.Do(request) fmt.Printf("client.do resp:%v, err:%#v\n" , response, err) rd := &respData{ resp: response, err: err, } respChan <- rd wg.Done() }() select { case <- ctx.Done(): fmt.Println("call api timeout" ) case result := <- respChan: fmt.Println("call server api success" ) if result.err != nil { fmt.Printf("call server qpi failed, err:%#v\n" , err) return } defer result.resp.Body.Close() data, _ := ioutil.ReadAll(result.resp.Body) fmt.Printf("resp:%#v\n" , string (data)) } }