Go 并发模式:上下文

未匹配的标注

本文为官方 Go Blog 的中文翻译,详见 翻译说明

Sameer Ajmani
29 July 2014

介绍

在 Go 的服务器程序中,每一个请求都有一个 goroutine 进行处理。然而,处理程序经常创建额外的 goroutines 去请求后端的数据库或者 RPC 服务等。这些处理请求的 goroutine 通常需要访问这个请求相关的值,例如最终用户的身份,认证 token 以及请求的期限。 当一个请求取消或超时时,处理该请求的所有goroutine 应该迅速退出,以便系统可以回收他们正在使用的任何资源。

在 Google,我们开发了一个 context package,可轻松跨过API边界,把request-scoped values,cancelation signal和deadline等传递给处理此请求的所有 goroutine。该软件包可作为context 公开获得。本文介绍了如何使用该程序包,并提供了一个完整的示例。

Context

context package 的核心是 Context 类型:

// 一个 Context 可以跨过API边界携带deadline, cancelation signal, 和 request-scoped values。 
// 它的方法可安全地被多个 goroutines 同时使用。
type Context interface {
    // Done 返回一个 channel,当这个 Context 取消或者超时时,此 channel 也被关闭。
    Done() <-chan struct{}

    // 在 Done channel 关闭之后,  Err 指出这个 context 取消的原因。
    Err() error

    // Deadline 返回这个 Context 取消的时间, 如果有的话。
    Deadline() (deadline time.Time, ok bool)

    // Value 返回与key关联的值,如果没有,返回 nil 。
    Value(key interface{}) interface{}
}

(这里的描述是简要的; 详细请参考 godoc.)

方法 Done 返回一个充当取消信号的 channel 给代表 Context 上下文中运行的函数:当 channel 关闭,这些函数应该马上放弃工作并返回。方法 Err 返回一个错误,用于指出 Context 关闭的原因。 Pipelines and Cancelation 文章详细讨论了 Done channel 惯用法。

Context 没有 Cancel 方法和 Done 仅仅是一个receive-only的 channel 的原因相同:接收cancelation signal的函数通常不是发送信号的函数。特别是,当父操作为子操作启动 goroutines 时,这些子操作不应该能够取消父操作。 相反,WithCancel 函数(下面讨论)提供了一个方法用来取消一个新的 Context 值。

Context对于多个 goroutines 同时使用是安全的。代码能够传递单个 Context 到任何数量的 goroutines 中,并且可以把取消这个 Context 的信号发送给所有的goroutines。

Deadline 方法允许函数判断它们是否开始工作:如果剩余时间太少,那可能不值得。代码也可以使用 deadline 来设置 I/O 超时。

Value 允许一个 Context 来携带request-scoped 数据。这个数据对于多个 goroutines 同时使用必须是安全的。

派生的 contexts

context package 提供了一些从现有 Context 值创建 派生 Context 值的函数。这些值行成一个树状结构:当一个 Context 被取消,所有从它派生的 Context 也都被取消。

Background 是所有 Context 树的根; 它从不取消:

// Background 返回一个空的Context. 它永远不会取消, 也没有deadline, 并且也没有值。
// ackground 经常用于 main, init, 和 tests里, 对于进来的请求来说是最顶层的 Context 。
func Background() Context

WithCancelWithTimeout 返回派生的 Context 值,这些值在它们的 parent Context 之前取消. 这些Context 会关联一个进来的请求,它们通常在请求处理程序返回时取消。WithCancel 在使用多个副本取消冗余时很有用。WithTimeout 用于在请求后端服务器时设置deadline:

// WithCancel 返回一个 parent的副本,当 parent.Done 关闭或者cancel被调用时,它的 Done channel 也马上关闭。
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// CancelFunc 取消一个 Context。
type CancelFunc func()

// WithTimeout 返回一个 parent的副本,当它的 parent.Done 关闭,cancel 被调用或者发生超时时,它的 Done channel 马上关闭。
// 返回新的 Context's deadline 是now+timeout 和它的 parent 的deadline的较早者。如果计时器仍在运行,则 cancel function 将释放其资源。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue 提供了一个方法把 request-scoped values和一个 Context 相关联:

// WithValue 返回一个 parent的副本,它的 Value 方法返回关于 key 的值.
func WithValue(parent Context, key interface{}, val interface{}) Context

使用 context package 最好的方式是看看下面的例子是如何使用的:

例子: Google 网页搜索

我们的例子是一个HTTP 服务器,用来处理类似/search?q=golang&timeout=1s的URLs。它依靠转发 "golang" 的查询到Google Web Search API 并渲染返回的结果。timeout 参数告诉服务器一段时间后取消此请求。

代码分为3个package:

  • server 提供了main 函数和 /search 处理程序.
  • userip 提供的函数从请求中提取用户的 IP 地址并和一个Context相关联.
  • google 提供了 Search 函数用来发送一个查询到 Google.

The server program

server 程序通过提供前几个 golang 的Google 搜索结果来处理类似 /search?q=golang 之类的请求。它注册handleSearch 来处理 /search 端点。这个处理程序创建一个称为 ctxContext 。并安排在处理程序返回时将其取消。 如果请求中包含 timeout URL参数,则在超时后会自动取消Context

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctx 是这个处理程序的Context。调用 cancel 关闭 ctx.Done channel, 当请求开始处理,它是取消信号。
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    timeout, err := time.ParseDuration(req.FormValue("timeout"))
    if err == nil {
        // 这个请求有 timeout,所以创建 context 用来在超时发生时自动取消
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
    defer cancel() // 当handleSearch 返回时取消 ctx 。

处理程序从请求中提取查询并通过调用 userip package来提取客户端的 IP 地址。客户端的 IP 地址是后端请求的需要,所以 handleSearch 会附加到 ctx

    // 检查搜索查询.
    query := req.FormValue("q")
    if query == "" {
        http.Error(w, "no query", http.StatusBadRequest)
        return
    }

    // 把用户 IP 存储在 ctx 中,给其他package代码使用。
    userIP, err := userip.FromRequest(req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    ctx = userip.NewContext(ctx, userIP)

这个处理程序使用 ctxquery 调用 google.Search :

    // 运行Google search 并打印结果。
    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
    }

Package userip

userip package 提供从请求中提取用户 IP 地址并关联一个 Context 的函数。Context 提供了key-value映射,并且key 和 value 都是 interface{} 类型。key 类型必须支持比较,values 必须在多个 goroutines 中同时使用是安全的。userip package 隐藏了映射的细节并提供了强类型(strongly-typed)来访问特殊的 Context 值。

为了避免 key 冲突, userip 定义了一个非导出类型 key 并且使用一个这个类型的一个值作为 context key:

// 这个 key 类型是非导出的,是为了避免和其他packages 定义的context key 冲突。
type key int

// userIPkey 作为 用户 IP 地址的 context key 。其值为0是任意的,如果这个package 
// 定义了其他context keys, 它们将有不同的整型值。
const userIPKey key = 0

FromRequesthttp.Request 中提取 userIP

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)
}

FromContextContext 提取userIP

func FromContext(ctx context.Context) (net.IP, bool) {
    // ctx.Value 返回 nil 如果 ctx 没有 相关 key 的值。
    // net.IP 类型断言 ok=false 返回 nil。
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}

Package google

google.Search 函数向Google Web Search API 发出 HTTP 请求并解析 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 APIs 使用用户 IP 来区分服务器发起的请求和最终用户的请求。
      if userIP, ok := userip.FromContext(ctx); ok {
        q.Set("userip", userIP.String())
    }
    req.URL.RawQuery = q.Encode()

Search 使用了一个辅助函数 httpDo 来发布 HTTP request。如果当请求或者相应处理过程中 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 函数在一个新 goroutine 里执行 HTTP 请求并处理它的响应。 如果在 goroutine 退出之前 ctx.Done 关闭,它取消请求:

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 // 等待 f 返回。
        return ctx.Err()
    case err := <-c:
        return err
    }
}

Contexts的适配代码

许多服务器框架为 request-scoped values提供了packages 和类型。我们能够定义 Context 接口一个新的实现来桥接使用现有框架的代码和期望使用 Context 参数的代码。

例如,Gorilla's github.com/gorilla/context package 允许处理程序通过提供从HTTP请求到键值对的映射来将数据与传入请求相关联。gorilla.go 中,我们提供了一个 Context 实现,它的 Value 方法返回与Gorilla package中的特定HTTP请求关联的值。

其他packages 提供了类似于 Context的取消支持。例如 Tomb 提供了一个 Kill 方法,该方法通过关闭Dying channel来发出取消信号。Tomb 还提供了等待 goroutine 退出的方法, 类似于 sync.WaitGroup。在tomb.go中,我们提供了一个 Context 实现,当 parent Context 被取消或者提供了 Tomb 被杀死,实现被取消。

结论

在Google,我们要求Go程序员将 Context 作为调用路径上每个函数的第一个参数传递。这使许多不同团队开发的Go代码可以很好地进行互操作。它提供了对超时和取消的简单控制,并确保安全凭证之类的关键值正确地传递给Go程序。

希望基于 Context 构建的服务器框架应提供 Context 的实现,以在其程序包和需要 Context 参数的程序包之间架起桥梁。然后,他们的客户端库将从调用代码中接受 Context。 通过为 request-scoped 的数据和取消操作建立公共接口,Context 使 package 程序开发人员更容易共享用于创建可伸缩服务的代码。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/go-blog/context...

译文地址:https://learnku.com/docs/go-blog/context...

上一篇 下一篇
Summer
贡献者:2
讨论数量: 0
发起讨论 只看当前版本


暂无话题~