go并发模式: Context
介绍
在 Go 服务器中,每个传入请求都在其自己的 goroutine 中处理。请求处理程序通常会启动额外的 goroutine 来访问数据库和 RPC 服务等后端服务。处理请求的一组 goroutine 通常需要访问特定于请求的值,例如最终用户的身份、授权令牌和请求的截止日期。当请求被取消或超时时,所有处理该请求的 goroutines 都应该快速退出,以便系统可以回收它们正在使用的任何资源。
在 Google,我们开发了一个context
包,可以轻松地将请求范围的值、取消信号和截止日期跨 API 边界传递给处理请求所涉及的所有 goroutine。该软件包作为context公开可用 。本文介绍了如何使用该包并提供了一个完整的工作示例。
语境
context
包的核心是Context
类型:
// 上下文携带截止日期、取消信号和请求范围的值
// 跨越 API 边界。
// 多个 goroutine同时使用它的方法是安全的。
type Context interface {
// Done 返回一个在取消此上下文时关闭的通道
// 或超时。
Done() <-chan struct{}
// Err 指示为什么在 Done 通道关闭后取消此上下文。
Err() error
// Deadline 返回取消此 Context 的时间(如果有)。
Deadline() (deadline time.Time, ok bool)
// Value 返回与 key 关联的值,如果没有则返回 nil。
Value(key interface{}) interface{}
}
(这个描述是浓缩的; godoc是权威的。)
该Done
方法返回一个通道,该通道充当代表运行的函数的取消信号Context
:当通道关闭时,函数应该放弃它们的工作并返回。该Err
方法返回一个错误,指示Context
取消的原因。Pipelines and Cancellation文章更Done
详细地讨论了通道习语。
出于同样的原因,通道是仅接收的,A 没有方法:接收Context
取消 信号的函数通常不是发送信号的函数*。*特别是,当父操作为子操作启动 goroutine 时,这些子操作不应该能够取消父操作。相反,WithCancel
函数(如下所述)提供了一种取消新值的方法。
AContext
对于多个 goroutine 同时使用是安全的。代码可以将单个传递Context
给任意数量的 goroutine 并取消它 Context
以向所有 goroutine 发出信号。(因为channel 是引用类型,内部实现用的指针)
该Deadline
方法允许函数确定它们是否应该开始工作;如果剩下的时间太少,可能就不值得了。代码也可以使用最后期限来设置 I/O 操作的超时。
Value
允许 aContext
携带请求范围的数据。该数据必须是安全的,以便多个 goroutine 同时使用。
派生上下文
该context
包提供了从现有context派生新context的功能。这些Context
形成一棵树:当 Context
被取消时,所有从它派生的Contexts
也被取消。
Background
是任何Context
树的根;它永远不会被取消:
// Backgrond 返回一个空的上下文。它永远不会被取消,没有截止日期,
// 并且没有值。Background通常用于 main、init 和测试,
// 并作为传入请求的顶级上下文。
func Background() Context
WithCancel 和 WithTimeout 返回派生的Context: 他们可以比 parent 更早取消。当请求处理程序返回时,通常会取消与传入请求关联 的Context。在使用多个副本时取消冗余请求使用WithCancel也很有用。 WithTimeout对于设置对后端服务器的请求的截止日期很有用:
// WithCancel 返回一个 parent 的副本,其 Done 通道在
parent.Done 关闭或调用 cancel 时立即关闭。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
// 一个 CancelFunc 取消一个上下文。
type CancelFunc func()
// WithTimeout 返回一个 parent 的副本,其 Done 通道在parent.Done 关闭、调用取消或超时结束时立即关闭。新
// 的上下文的截止日期是 now+timeout 和父级的截止日期中较早的一个(如果有的话)
// 如果计时器仍在运行,则取消函数释放其资源。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
WithValue
提供了一种将请求范围的值与 a Context
相关联的方法:
// WithValue 返回其 Value 方法为 key 返回 val 的 parent 的副本。
func WithValue(parent Context, key interface{}, val interface{}) Context
查看如何使用该context
软件包的最佳方法是通过一个工作示例。
示例:谷歌网页搜索
我们的示例是一个 HTTP 服务器,它 处理/search?q=golang&timeout=1s
的URLs, 通过将查询“golang”转发到 Google Web Search API并呈现结果来处理 URL。该timeout
参数告诉服务器在该持续时间过去后取消请求。
代码分为三个包:
- 服务器. main入口,并处理 /search 请求
- userip提供从请求中提取用户 IP 地址并将其与
Context
关联的方法。 - google提供了
Search
函数,以便向 Google 发送查询的功能。
服务器程序
服务器程序处理请求 ,例如/search?q=golang
为golang
. 它注册handleSearch
以处理/search
端点。处理程序创建一个初始Context
调用ctx
并安排在处理程序返回时取消它。如果请求包含timeout
URL 参数,Context
则在超时后自动取消:
func handleSearch(w http.ResponseWriter, req *http.Request) {
// ctx 是这个处理程序的上下文。调用 cancel 会关闭
// ctx.Done 通道,该通道是
// 由该处理程序启动的请求的取消信号。
var (
ctx context.Context
cancel context.CancelFunc
)
timeout, err := time.ParseDuration(req.FormValue("timeout"))
if err == nil {
// 请求有超时,所以创建一个
// 当超时到期时自动取消的上下文。
ctx, cancel = context.WithTimeout(context.Background(), timeout)
} else {
ctx, cancel = context.WithCancel(context.Background())
}
defer cancel() // handleSearch 返回后立即取消 ctx。(在一个复杂的业务场景中,需要出错即返回,已发起的gorotuine没必要再运行了)
处理程序从请求中提取查询,并通过调用userip
包来提取客户端的 IP 地址。后端请求需要客户端的 IP 地址,因此handleSearch
将其附加到ctx
:
// 检查搜索查询。
query := req.FormValue("q")
if query == "" {
http.Error(w, "no query", http.StatusBadRequest)
return
}
// 将用户 IP 存储在 ctx 中,以供其他包中的代码使用。
userIP, err := userip.FromRequest(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
ctx = userip.NewContext(ctx, userIP)
google.Search
处理程序使用ctx
和调用query
:
// 运行 Google 搜索并打印结果。
start := time.Now()
results, err := google.Search(ctx, query)
elapsed := time.Since(start)
如果搜索成功,则处理程序呈现结果:
if err := resultsTemplate.Execute(w, struct {
Results google.Results
Timeout, Elapsed time.Duration
}{
Results: results,
Timeout: timeout,
Elapsed: elapsed,
}); err != nil {
log.Print(err)
return
}
包用户ip
所述USERIP包提供功能,用于从请求中提取用户的IP地址,并将其与一个相关联Context
。AContext
提供了一个键值映射,其中键和值都是 type interface{}
。键类型必须支持相等,并且值必须安全地被多个 goroutine 同时使用。诸如userip
隐藏此映射的详细信息并提供对特定Context
值的强类型访问的包。
为避免键冲突,userip
定义一个未导出的类型key
并使用此类型的值作为上下文键:
// 键类型未导出以防止与
// 其他包中定义的上下文键发生冲突。
type key int
// userIPkey 是用户 IP 地址的上下文键。它的零值是
// 任意的。如果此包定义了其他上下文键,它们将具有
// 不同的整数值。
const userIPKey key = 0
FromRequest
从中提取一个userIP
值http.Request
:
func FromRequest(req *http.Request) (net.IP, error) {
ip, _, err := net.SplitHostPort(req.RemoteAddr)
if err != nil {
return nil, fmt.Errorf("userip: %q is not IP:port", req.RemoteAddr)
}
NewContext
返回一个Context
带有提供值的新userIP
值:
func NewContext(ctx context.Context, userIP net.IP) context.Context {
return context.WithValue(ctx, userIPKey, userIP)
}
FromContext
从 Context中提取userIP
func FromContext(ctx context.Context) (net.IP, bool) {
// 如果 ctx 没有 key 的值,ctx.Value 返回 nil;
// net.IP 类型断言为 nil 返回 ok=false。
userIP, ok := ctx.Value(userIPKey).(net.IP)
return userIP, ok
}
Package google
该google.Search功能使一个HTTP请求到谷歌网页搜索API 和解析JSON编码的结果。它接受一个Context
参数ctx
,如果ctx.Done
在请求运行时关闭,则立即返回。
Google Web Search API 请求包括搜索查询和用户 IP 作为查询参数:
func Search(ctx context.Context, query string) (Results, error) {
// 准备 Google Search API 请求。
req, err := http.NewRequest("GET", "https://ajax.googleapis.com/ajax/services/search/web?v=1.0", nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
q.Set("q", query)
// 如果ctx携带的是用户IP地址,则转发给服务器。
// Google API 使用用户 IP 来区分服务器发起的请求
// 与最终用户请求。
if userIP, ok := userip.FromContext(ctx); ok {
q.Set("userip", userIP.String())
}
req.URL.RawQuery = q.Encode()
Search
使用辅助函数 httpDo
发出 HTTP 请求,如果ctx.Done
在处理请求或响应时关闭,则取消它。 Search
传递一个闭包来httpDo
处理 HTTP 响应:
var results Results
err = httpDo(ctx, req, func(resp *http.Response, err error) error{
if err != nil {
return err
}
defer resp.Body.Close()
// 解析 JSON 搜索结果。
// https://developers.google.com/web-search/docs/#fonje
var data struct {
ResponseData struct {
Results []struct {
TitleNoFormatting string
URL string
}
}
}
if err := json.NewDecoder(resp.Body).Decode(&data); err !=nil {
return err
}
for _, res := range data.ResponseData.Results {
results = append(results, Result{Title: res.TitleNoFormatting, URL: res.URL})
}
return nil
})
// httpDo 等待我们提供的闭包返回,所以
return results, err
该httpDo
函数运行 HTTP 请求并在新的 goroutine 中处理其响应。ctx.Done
如果在 goroutine 退出之前关闭,它将取消请求:
// httpDo issues the HTTP request and calls f with the response. If ctx.Done is
// closed while the request or f is running, httpDo cancels the request, waits
// for f to exit, and returns ctx.Err. Otherwise, httpDo returns f's error.
func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
// 在goroutine中运行HTTP请求并将响应传递给f。
c := make(chan error, 1)
req = req.WithContext(ctx)
go func() { c <- f(http.DefaultClient.Do(req)) }()
select {
case <-ctx.Done():
<-c // Wait for f to return.
return ctx.Err()
case err := <-c:
return err
}
}
为上下文调整代码
许多服务器框架提供包和类型来承载请求范围的值。我们可以定义接口的新实现,Context
以在使用现有框架的代码和需要Context
参数的代码之间架起桥梁。
例如,Gorilla 的 github.com/gorilla/context 包允许处理程序通过提供从 HTTP 请求到键值对的映射来将数据与传入请求相关联。在gorilla.go中,我们提供了一个Context
实现,其Value
方法返回与 Gorilla 包中特定 HTTP 请求关联的值。
其他软件包提供了类似于Context
. 例如,Tomb提供了一种通过关闭通道 Kill
来发出取消信号的方法。还提供了等待这些 goroutine 退出的方法,类似于 . 在tomb.go中,我们提供了一个实现,当它的父对象被取消或提供的被杀死时,该实现被取消。Dying``Tomb``sync.WaitGroup``Context``Context``Tomb
结论
在 Google,我们要求 Go 程序员将Context
参数作为第一个参数传递给传入和传出请求之间的调用路径上的每个函数。这使得许多不同团队开发的 Go 代码能够很好地互操作。它提供了对超时和取消的简单控制,并确保安全凭证等关键值正确传输 Go 程序。
想要构建的服务器框架Context
应该提供Context
在其包和期望Context
参数的包之间架起桥梁的实现。然后,他们的客户端库将接受Context
来自调用代码的 a。通过为请求范围的数据和取消建立一个通用接口, Context
包开发人员可以更轻松地共享代码以创建可扩展的服务。