Go 译文之经过 context 实现并发控制

做者:Sameer Ajmani | 地址:blog.golang.org/contextgit

译者前言

第二篇官方博客的翻译,主要是关于 Go 并发控制的 context 包。github

整体来讲,我认为上一篇才是 Go 并发的基础与核心。context 是在前章基础之上,为 goroutine 控制而开发的一套便于使用的库。毕竟,在不一样的 goroutine 之间只传递 done channel,包含信息量确实是太少。golang

文章简单介绍了 context 提供的方法,以及简单介绍它们如何使用。接着,经过一个搜索的例子,介绍了在真实场景下的使用。web

文章的尾部部分说明了,除了官方实现的 context,也有一些第三方的实现,好比 github.com/contextTomb,但这些在官方 context 出现以后就已经中止更新了。其实缘由很简单,毕竟通常都是官方更强大。以前,go 模块管理也是百花齐放,但最近官方推出本身的解决方案,或许不久,其余方式都将会淘汰。ajax

其实,我以为这篇文章并很差读,感受不够按部就班。忽然的一个例子或许会让人有点懵逼。数据库

翻译正文以下:json


Go 的服务中,每一个请求都会有独立的 goroutine 处理,每一个 goroutine 一般会启动新的 goroutine 执行一些额外的工做,好比进行数据库或 RPC 服务的访问。同请求内的 goroutine 需能共享请求数据访问,好比,用户认证,受权 token,以及请求截止时间。若是请求取消或发生超时,请求范围内的全部 goroutine 都应马上退出,进行资源回收.后端

在 Google,咱们开发了一个 context 的包,经过它,咱们能够很是方便地在请求内的 goroutine 之间传递请求数据、取消信号和超时信息。详情查看 contextapi

本文将会具体介绍 context 包的使用,并提供一个完整的使用案例。安全

Context

context 的核心是 Context 类型。定义以下:

// A Context carries a deadline,cancellation signal,and request-scoped values
// across API. Its methods are safe for simultaneous use by multiple goroutines
// 一个 Context 能够在 API (不管是不是协程间) 之间传递截止日期、取消信号、请求数据。
// Context 中的方法都是协程安全的。
type Context interface {
    // Done returns a channel that is closed when this context is cancelled
    // or times out.
    // Done 方法返回一个 channel,当 context 取消或超时,Done 将关闭。
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed
    // 在 Done 关闭后,Err 可用于代表 context 被取消的缘由
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    // 到期则取消 context
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none
    Value(key interface{}) interface{}
}
复制代码

介绍比较简要,详细信息查看 godoc

Done 方法返回的是一个 channel,它可用于接收 context 的取消信号。当 channel 关闭,监听 Done 信号的函数会马上放弃当前正在执行的工做并返回。Err 方法返回一个 error 变量,从它之中能够知道 context 为何被取消。pipeline and cancelation 一文对 Done channel 做了详细介绍。

为何 Context 没有 cancel 方法,它的缘由与 Done channel 只读的缘由相似,即接收取消信号的 goroutine 经过不会负责取消信号的发出。特别是,当父级启动子级 goroutine 来执行操做,子级是没法取消父级的。反之,WithCancel 方法(接下来介绍)提供了一种方式取消新建立的 Context。

Context 是协程并发安全的。咱们能够将 Context 传递给任意数量的 goroutine,经过 cancel 能够给全部的 goroutine 发送信号。

Deadline 方法可让函数决定是否须要启动工做,若是剩余时间过短,那么启动工做就不值得了。在代码中,咱们能够经过 deadline 为 IO 操做设置超时时间。

Value 方法可让 context 在 goroutine 之间共享请求范围内的数据,这些数据须要是协程并发安全的。

派生 Context

context 包提供了多个函数从已有的 Context 实例派生新的 Context。这些 Context 将会造成一个树状结构,只要一个 Context 取消,派生的 context 将都被取消。

Background 函数返回的 Context 是任何 Context 根,而且不能够被取消。

// 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.
// Background 函数返回空 Context,而且不能够取消,没有最后期限,没有共享数据。Background 仅仅会被用在 main、init 或 tests 函数中。
func Background() Context
复制代码

WithCancel 和 WithTimeout 会派生出新的 Context 实例,派生实例比父级更早被取消。与请求关联的 Context 实例,在请求处理完成后将被取消。当遇到多副本的数据请求时,WithCancel 可用于取消多余请求。在请求后端服务时,WithTimeout 可用于设置超时时间。

// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
// WithCanal 返回父级 Context 副本,当父级的 Done channel 关闭或调用 cancel,它的 Done channel 也会关闭。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) // A CancelFunc cancels a Context. // CancelFunc 用于取消 Context type CancelFunc func() // WithTimeout returns a copy of parent whose Done channel is closed as soon as // parent.Done is closed, cancel is called, or timeout elapses. The new // Context's Deadline is the sooner of now+timeout and the parent's deadline, if // any. If the timer is still running, the cancel function releases its // resources. // 返回父级 Context 副本和 CancelFunc,三种状况,它的 Done 会关闭,分别是父级 Done 关闭,cancel 被调用,和达到超时时间。 func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) 复制代码

WithValue 提供了一种方式,经过 Context 传递请求相关的数据

// WithValue returns a copy of parent whose Value method returns val for key.
func WithValue(parent Context, key interface{}, val interface{}) Context 复制代码

context 如何使用呢?最好的方式,经过一个案例演示。

案例:Google Web 搜索

演示一个案例,实现一个 HTTP 服务,处理相似 /search?q=golang&timeout=1s 的请求。timeout 表示若是请求处理时间超过了指定时间,取消执行。

代码主要涉及 3 个 package,分别是:

  • server,主函数入口和 /search 处理函数;
  • userip,实现从 request 的 context 导出 user ip 的公共函数;
  • google,实现了 Search 函数,负责向 Google 发送搜索请求;

开始介绍!

Package server

server 负责处理相似 /search?q=golang 的请求,返回 golang 搜索结果,handleSearch 是实际的处理函数,它首先初始化了一个 Context,命名为 ctx,经过 defer 实现函数退出 cancel。若是请求参数含有 timeout,经过 WithTimeout 建立 context,在超时后,Context 将自动取消。

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctx is the Context for this handler. Calling cancel closes the
    // cxt.Done channel, which is the cancellation signal for requests
    // started by this handler
    var (
        ctx context.Context
        cancel context.Context
    )

    timeout, err := time.ParseDuration(req.FromValue("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())
    }
    defer cancel() // Cancel ctx as soon as handlSearch returns.
复制代码

下一步,处理函数会从请求中获取查询关键词和客户端 IP,客户端 IP 的获取经过调用 userip 包函数实现。同时,因为后端服务的请求也须要客户端 IP,故而将其附在 ctx 上。

// Check the search query
    query := req.FormValue("q")
    if query == "" {
        http.Error(w, "no query", http.StatusBadRequest)
        return
    }

    // Store the user IP in ctx for use by code in other packages.
    userIP, err := userip.FormRequest(req)
    if err != nil {
        http.Error(w, e.Error(), http.StatusBadRequest)
        return
    }

    ctx = userip.NewContext(ctx, userIP)
复制代码

调用 google.Search,并传入 ctx 和 query 参数。

// Run the Google search and print the results
    start := time.Now()
    results, err := google.Search(ctx, query)
    elapsed := time.Since(start)
复制代码

搜索成功后,handler 渲染结果页面。

if err := resultsTemplate.Execute(w, struct{
        Results     google.Results
        Timeout, Elapsed time.Duration
    }{
        Results: results,
        Timeout: timeout,
        Elaplsed: elaplsed,
    }); err != nil {
        log.Print(err)
        return
    }
复制代码

Package userip

userip 包中提供了两个函数,负责从请求中导出用户 IP 和将用户 IP 绑定 Context 上。 Context 中包含 key-value 映射,key 与 value 的类型都是 interface{},key 必须支持相等比较,value 要是协程并发安全的。userip 包经过对 Context 中的 value ,即 client IP 执行了类型转化,隐藏了 map 的细节。为了不 key 的冲突,userip 定义了一个不可导出的类型 key。

// The key type is unexported to prevent collision with context keys defined in
// other package
type key int

// userIPkey is the context key for the user IP address. Its value of zero is
// arbitrary. If this package defined other context keys, they would have
// different integer values.
const userIPKye key = 0
复制代码

函数 FromRequest 负责从 http.Request 导出用户 IP:

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 生成一个带有 userIP 的 Context:

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.Value returns nil if ctx has no value for the key;
    // the net.IP type assertion returns ok=false for nil
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}
复制代码

Package google

google.Search 负责 Google Web Search 接口的请求,以及接口返回 JSON 数据的解析。它接收 Context 类型参数 ctx,若是 ctx.Done 关闭,即便请求正在运行也将马上返回。

查询的请求参数包括 query 关键词和用户 IP。

func Search(ctx context.Context, query string) (Results, error) {
    // Prepare the Google Search API request
    req, err := http.NewRequest("GET", "http://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)

    // If ctx is carrying the user IP address, forward it to the server
    // Google APIs use the user IP to distinguish server-initiated requests 
    // from end-users requests
    if userIP, ok := userip.FromContext(ctx); ok {
        q.Set("userip", userIP.String())
    }
    req.URL.RawQuery = q.Encode()
复制代码

Search 函数使用了一个帮助函数,httpDo,负责发起 HTTP 请求,若是 ctx.Done 关闭,即便请求正在执行,也会被关闭。Search 传递了一个闭包函数给 httpDo 处理响应结果。

var results Results
    err = httpDo(ctx, req, func(resp *http.Response, err error) error {
        if err != nil {
            return err
        }
        defer resp.Body.Close()

        // Parse the JSON search result.
        // 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
    })

    return results, err
复制代码

httpDo 函数开启一个新的 goroutine 负责 HTTP 请求执行和响应结果处理。若是在 goroutine 退出前,即请求还没执行结束,若是 ctx.Done 关闭,请求执行将被取消。

func httpDo(ctx context.Context, req *http.Request, f func(*http.Request, error) error) error {
    // Run the HTTP request in a goroutine and pass the response to f.
    c := make(chan error, 1)
    req := req.WithContext(ctx)
    go func() { c <- f(http.DefaultClient.Do(req)) }()
    select {
    case <-ctx.Done():
        <- c
        return ctx.Err
    case  err := <-c:
        return err
    }
}
复制代码

基于 Context 调整代码

许多服务端框架都提供了相应的包和数据类型进行请求的数据传递。咱们能够基于 Context 接口编写新的实现代码,完成框架与处理函数的链接。

译者注:下面介绍的就是开发说的两个 context 的第三方实现,其中有些内容须要简单了解下它们才能彻底看懂。
复制代码

例如,Gorilla's 的 context 经过在请求上提供 key value 映射实现关联数据绑定。在 gorilla.go,提供了 Context 的实现,它的 Value 方法返回的值和一个具体的 HTTP 请求关联。

其余一些包提供与 Context 相似的取消支持。例如,Tomb 中有 Kill 方法经过关闭 Dying channel 实现取消信号发出。Tomb 也提供了方法用于等待 goroutine 退出,与 sync.WaitGroup 相似。在 tomb.go 中,提供了一种实现,当父 Context 取消或 Tomb 被 kill时,当前 Context 将会取消。

总结

在 Google,对于接收或发送请求类的函数,咱们要求必需要将 Context 做为首个参数进行传递。如此,即便不一样团队的 Go 代码也能够工做良好。Context 很是便于 goroutine 的超时与取消控制,以及确保重要数据的安全传递,好比安全凭证。

基于 Context 的服务框架须要实现 Context,帮助链接框架和使用方,使用方指望从框架接收 Context 参数。而客户端库,则与之相反,它从调用方接收 Context 参数。context 经过为请求数据与取消控制创建通用接口,实现包开发者们能够很是轻松地共享本身的代码,以及打造出更具扩展性的服务。


波罗学的微信公众号
相关文章
相关标签/搜索