references:
介绍
在go服务中,每一个进来的请求都由自己的goroutine来负责处理,请求处理器经常启动额外的goroutine来访问后端服务,比如服务器或者RPC服务
一般情况下,goroutine在请求处理的时候需要访问该请求相关的一些特定数据,比如身份信息、认证token和请求时效,当请求取消或者超时之后,所有处理该请求的goroutine都应该快速退出,以便系统能够尽快地回收他们所占用的资源
Google开发了一个context包以便更简单便捷地传递只在该请求中有效的一些数据,context可以打破goroutine的API边界,以在更大的范围内共享数据,本文将会提供完整的代码样例以演示context的使用
context
context包的核心就是Context类型,其定义如下:
// A Context carries a deadline, cancellation 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没有Cancel函数,和Done频道是只接收的一个原因,会收到取消信号的函数一般都不会发送信号
Context对象是并发安全的,可以将Context传递给多个goroutine,取消这个Context即可通知所有的goroutine
Deadline函数用于判断是否应该开始工作,如果剩余时间太短,那么就不值当再去执行了,实际的代码可能会使用deadline来为IO操作设置超时时间
Value允许Context携带请求相关的数据,针对这些数据的操作必须为并发安全的
派生context
context包提供了一个函数可以用于从已经存在的context中派生新的context值,这些值来自于一棵树,当一个Context被取消之后,所有的Context派生的context也都会被取消
Background函数是Context树的root,他永远不会被取消
// 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通常会在handler返回之后被取消
WithCancel is also useful for canceling redundant requests when using multiple replicas(没看懂)
WithTimeout可以设置deadline
WithValue提供了一种将请求范围内有效的值和Context关联起来的手段
// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context
例子,Google web search
本例为一个HTTP server,他会处理这样的URL:
/search?q=golang&timeout=1s
他会将这个“golang”请求转发给Google Web Search API并渲染结果,timeout参数告诉服务器在这个时间之后取消该请求
代码被划分到3个包里面:
- server包,提供main方法,以及/search的处理代码
- userip包,从请求中提取用户IP地址并将其关联到Context中
- google包,提供search函数,将请求发送给Google
服务端程序
服务器处理/search?q=golang这样的请求,他注册了handleSearch函数来处理/search端点,处理代码创建一个初始Context,叫做ctx并将其设置为在处理代码返回之后取消
// 使用http库的HandleFunc注册/search模式的路径到handleSearch函数
http.HandleFunc("/search", handleSearch)
如果设置了超时参数就在超时后自动取消
timeout, err := time.ParseDuration(req.FormValue("timeout"))
if err == nil {
// The request has a timeout, so create a context that is
// canceled automatically when the timeout expires.
ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
context.WithTime和context.WithCancel都会返回一个从context.Background()(根Context)派生出来的子context
前者会在超时后自动取消,也可以使用返回的cancel函数主动取消
后者只能使用cancel函数主动取消
下一行代码是defer cancel() // Cancel ctx as soon as handleSearch returns.
这行代码可以保证在该函数返回之前调用cancel函数来取消context,避免资源的泄漏
然后我们需要获取到用户的IP并将其存储在ctx中
userIP, err := userip.FromRequest(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx = userip.NewContext(ctx, userIP)
NewContext使用如下代码存储数据
return context.WithValue(ctx, userIPKey, userIP)
需要我们自定义一个key
const userIPKey key = 0
key的值我们可以随便写,别在同一个context里面冲突就行
context.WithValue会返回一个新的context,原来的context就变成了新context的父亲,构建在树上,如下所示
context.Background() root节点
context 儿子节点
新context 孙子节点
当我们访问新context的value时:ctx.Value(key),那么他就会先查询新context本身是否有这个key,如果没有就往上遍历,直到root节点,也就是说,原来的context被嵌入到了新context中
req = req.WithContext(ctx)
go func() { c <- f(http.DefaultClient.Do(req)) }()
执行完WithContext函数之后,如果ctx中一开始设置了timeout,则http.DefaultClient.Do会在超时的时候返回
context的特别之处
考虑下面这个例子:
package main
import (
"fmt"
"time"
)
type StopSignal struct {
Stop bool
}
func worker(id int, sig *StopSignal) {
for i := 0; i < 10; i++ {
if sig.Stop {
fmt.Printf("worker %d stopped manually\n", id)
return
}
fmt.Printf("worker %d working %d\n", id, i)
time.Sleep(200 * time.Millisecond)
}
}
func main() {
sig := &StopSignal{}
for i := 1; i <= 3; i++ {
go worker(i, sig)
}
time.Sleep(1 * time.Second)
sig.Stop = true // stop signal
time.Sleep(500 * time.Millisecond)
fmt.Println("main done")
}
我们所创建的3个goroutine全部通过循环检查sig变量来确定自己什么时候应该退出,而如果我们引入context,就会变得简单和高效
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
fmt.Printf("worker %d stopped: %v\n", id, ctx.Err())
return
default:
fmt.Printf("worker %d working %d\n", id, i)
time.Sleep(200 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
// no need to manually signal stop
time.Sleep(2 * time.Second)
fmt.Println("main done")
}
所有goroutine会在超时后自动退出
也可以手动取消
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, id int) {
for i := 0; i < 1000; i++ {
select {
case <-ctx.Done():
fmt.Printf("worker %d stopped: %v\n", id, ctx.Err())
return
default:
fmt.Printf("worker %d working %d\n", id, i)
time.Sleep(200 * time.Millisecond)
}
}
}
func main() {
var (
ctx context.Context
cancel context.CancelFunc
)
ctx, cancel = context.WithCancel(context.Background())
for i := 1; i <= 3; i++ {
go worker(ctx, i)
}
time.Sleep(2 * time.Second)
cancel()
// no need to manually signal stop
time.Sleep(100 * time.Second)
fmt.Println("main done")
}