微信号:go-programming-lang

介绍:重点介绍go语言的高级功能,比如interface、 reflection、goroutine、concurrency、memory model、testing、functional、toolchains等.

Go语言并发模型:使用 context

2016-08-29 00:43 Oscar 译

简介

Go http包的Server中,每一个请求在都有一个对应的 goroutine 去处理。


请求处理函数通常会启动额外的 goroutine 用来访问后端服务,比如数据库和RPC服务。


用来处理一个请求的 goroutine 通常需要访问一些与请求特定的数据,比如终端用户的身份认证信息、验证相关的token、请求的截止时间。


当一个请求被取消或超时时,所有用来处理该请求的 goroutine 都应该迅速退出,然后系统才能释放这些 goroutine 占用的资源。


Google 内部,我们开发了Context包,专门用来简化 对于处理单个请求的多个 goroutine 之间与 请求域的数据、取消信号、截止时间等相关操作,这些操作可能涉及调用多个 API


你可以通过go get golang.org/x/net/context命令获取这个包。本文要讲的就是如果使用 这个包,同时也会提供一个完整的例子。

阅读建议

本文内容涉及到了 done channel,如果你不了解这个概念,那么请先阅读本公众号之前的一篇文章 “Go语言并发模型:像Unix Pipe那样使用channel”


由于访问 golang.org/x/net/context 需要梯子,你可以访问它在github 上的 mirror (相关链接->mirror of package net)。如果要下载本文中的代码,可以查看文章末尾的相关链接环节。


如果有条件的话,建议将代码下载下来以后,阅读代码以后再看这篇文章。

package context

context 包的核心是 struct Context,声明如下:


//
一个 Context 对象可以携带截止时间信号
// 取消信号、和跨多个API的请求作用域数据
// 它的方法是线程安全的。
type Context interface {
   // Done
返回一个 channel,该 channel
   // Context 对象被取消时关闭
    Done() <-chan struct{}
   
    // Err
方法返回 Done channel 关闭后,
   // Context
对象被取消的原因
    Err() error
   
   // Deadline
方法返回 Context 被取消的时间
    Deadline() (deadline time.Time, ok bool)
 
    // Value
方法与key对应的value,或 nil
    Value(key interface{}) interface{}
}


注意: 这里我们对描述进行了简化,更详细的描述查看 context 的 godoc。


Done方法返回一个 channel,这个 channel 对于以 Context方式运行的函数而言,是一个取消信号。


当这个 channel 关闭时,上面提到的这些函数应该终止手头的工作并立即返回。


之后,Err方法会返回一个错误,告知为什么 Context被取消。


关于Done channel 的更多细节查看本公众号之前的一篇文章 “Go语言并发模型:像Unix Pipe那样使用channel”


一个 Context不能拥有Cancel方法,同时我们也只能 Done channel接收数据。


背后的原因是一致的:接收取消信号的函数 和发送信号的函数通常不是一个。


一个典型的场景是:父操作为子操作启动 goroutine,子操作也就不能取消父操作。


作为一个折中WithCancel函数 (后面会细说) 提供了一种取消子 Context 对象的方法。


Context对象是线程安全的,你可以把一个 Context对象传递给任意个数的 gorotuine 对它执行取消 操作时,所有 goroutine 都会接收到取消信号。


Deadline方法允许函数确定它们是否应该开始工作。如果剩下的时间太少,也许这些函数就不值得启动。代码中,我们也可以使用 Deadline对象为 I/O 操作设置截止时间。


Value方法允许 Context对象携带request作用域的数据,该数据必须是线程安全的。

继承 context

context 包提供了一些函数,协助用户从现有的Context对象创建新的 Context对象。这些 Context对象形成一棵树:当一个Context对象被取消时,继承自它的所有 Context都会被取消。


Background是所有 Context对象树的根,它不能被取消。它的声明如下:

// Background 返回一个空 Context
//
Context 不能被取消,也没有
// 截止时间,也不能存数据
// Background 通常用在 main, init
// 和测试代码里,作为入口请求的顶层 Context
func Background() Context


WithCancel WithTimeout函数会返回继承的 Context对象,这些对象可以比它们的父 Context更早地取消。


当请求处理函数返回时,与该请求关联的 Context会被取消。当使用多个副本发送请求时,可以使用 WithCancel取消多余的请求。


WithTimeout在设置对后端服务器请求截止时间时非常有用。 下面是这三个函数的声明:

// WithCancel 返回父 Context(parent) 的拷贝
// 一旦parent.Done channel关闭,或
// 该拷贝被
cancel,Done channel也被关闭
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
 
// CancelFunc
可以取消一个 Context
type CancelFunc func()

// WithTimeout
返回父Context(parent)的拷贝
// 一旦parent.Done 被关闭、该拷贝的cancel函数
// 被调用或 timeout 到期,它的Done channel随之关闭。
// Context 如果有 Deadline,必然早于
// now+timeout
,也早于父 Context Deadline
// 此时如果计时器还在运行,cancel函数会释放资源。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)


WithValue函数能够将请求作用域的数据与 Context对象建立关系。声明如下:

// WithValue返回父 Context 的一个拷贝
// 它的 Value 方法返回对应key value
func WithValue(parent Context, key interface{}, val interface{}) Context

当然,想要知道 Context包是如何工作的,最好的方法是看一个例子。

一个例子: Google Web Search

我们的例子是一个 HTTP 服务,它能够将类似于/search?q=golang&timeout=1s的请求 转发给Google Web Search API (见“相关链接”),然后渲染返回的结果。timeout参数用来告诉 server 取消请求的时间。


这个例子的代码存放在三个包里:

  1. server:它提供 main 函数和 处理 /search 的 http     handler

  2. userip:它能够从请求解析用户的IP,并将请求绑定到一个 Context 对象。

  3. google:它包含了 Search 函数,用来向 Google 发送请求。

深入 server

server 程序处理类似于 /search?q=golang的请求,返回 Google API 的搜索结果。它将handleSearch函数注册到 /search路由。


处理函数创建一个Context ctx,并对其进行初始化, 以保证Context取消时,处理函数返回。


如果请求的 URL 参数中包含 timeout,那么当 timeout 到期时, Context会被自动取消。 handleSearch 的代码如下:


func handleSearch(w http.ResponseWriter, req *http.Request) {
   // ctx
是该处理函数的 Context
   //
调用 cancel 函数会关闭 ctx.Done,然后
    // 该函数发起的请求都会收到一个取消信号
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    timeout, err := time.ParseDuration(req.FormValue("timeout"))
    if err == nil {
        //
该请求有一个 timeout,所以创建的
        // Context timeout 到期时会自动取消。
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
    defer cancel() // handleSearch
一旦返回,ctx 就会被cancel

处理函数(handleSearch) query 参数从请求中解析出来,然后通过 userip 包将client IP解析出来。


这里 Client IP 后端发送请求时要用到,所以 handleSearch 函数将它 attach Context对象 ctx 上。代码如下:

// 检查搜索参数
query := req.FormValue("q")
if query == "" {
    http.Error(w, "no query", http.StatusBadRequest)
    return
}
 
//
user IP存储在 ctx 变量中方便其他package使用
userIP, err := userip.FromRequest(req)
if err != nil {
    http.Error(w, err.Error(), http.StatusBadRequest)
    return
}
ctx = userip.NewContext(ctx, userIP)

处理函数带着 Context对象ctx query调用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
}

深入 userip 包

userip 包提供了两个功能:

  1. 从请求解析出 Client IP

  2. Client IP 关联到一个 Context对象。


一个Context对象提供一个key-value 映射,key value 的类型都是 interface{},但是 key 必须满足等价性(可以比较),value必须是线程安全的。


类似于 userip的包隐藏了映射的细节,提供的是对特定 Context类型值得强类型访问。


为了避免 key 冲突,userip定义了一个非输出类型key,并使用该类型的值作为Contextkey。代码如下:

// 为了避免与其他包中的 Context key 冲突
// 这里不输出 key 类型 (首字母小写)
type key int
 
// userIPKey
user IP Context key
//
它的值是随意写的。如果这个包中定义了其他
// `Context` key,这些 key 必须不同
const userIPKey key = 0

函数 FromRequest用来从一个 http.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)
}

函数 FromContext从一个Context对象中解析 userIP

func FromContext(ctx context.Context) (net.IP, bool) {
    // 如果ctx 中找不到这个 key,ctx.Value 返回 nil
    // 此时net.IP 类型断言返回 ok=false
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}

深入 google 包

函数 google.Search Google Web Search API 发送一个 HTTP 请求,并解析返回的 JSON数据。


该函数接收一个Context对象 ctx 作为第一参数,在请求还没有返回时,一旦ctx.Done关闭,该函数也会立即返回。


Google Web Search API 请求包含 query 关键字和 user 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 中包含 user IP,则发送给 server
   // Google API
使用 user 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在一个新的 goroutine 中发送 HTTP 请求和处理结果。


如果ctx.Done已经关闭, 而处理请求的 goroutine 还存在,那么取消请求。下面是具体实现:


func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, error) error) error {
   //
在一个 goroutine 中发送 HTTP 请求
   // 并将结果交给f 处理
    tr := &http.Transport{}
    client := &http.Client{Transport: tr}
    c := make(chan error, 1)
    go func() { c <- f(client.Do(req)) }()
    select {
    case <-ctx.Done():
        tr.CancelRequest(req)
        <-c // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }
}

在自己的代码中使用 Context

许多服务器框架都提供了管理请求作用域数据的包和类型。我们可以定义一个Context接口的实现, 将已有代码和期望Context参数的代码粘合起来。


Gorilla 框架的github.com/gorilla/context 允许处理函数(handlers) 将数据和请求结合起来,他通过HTTP 请求 key-value 的映射来实现。


gorilla.go中,我们提供了一个 Context的具体实现,这个实现的 Value 方法返回的值已经与 gorilla 包中特定的 HTTP 请求关联起来。


还有一些包实现了类似于 Context的取消机制。比如Tomb中有一个 Kill 方法,该方法通过关闭 名为Dying channel 发送取消信号。


Tomb也提供了等待 goroutine 退出的方法,类似于 sync.WaitGroup tomb.go 中,我们提供了一个Context的实现,当它的父 Context被取消 一个Tomb对象被 kill 时,该Context对象也会被取消。

总结

Google我们要求 Go 程序员把Context作为第一个参数传递给 入口请求和出口请求链路上的每一个函数。


这样机制一方面保证了多个团队开发的 Go 项目能够良好地协作,另一方面它是一种简单的超时和取消机制, 保证了临界区数据 (比如安全凭证) 在不同的 Go 项目中顺利传递。


如果你要在 Context之上构建服务器框架,需要一个自己的Context实现,在框架与期望 Context参数的代码之间建立一座桥梁。


当然,Client 库也需要接收一个Context对象。在请求作用域数据与取消之间建立了通用的接口以后,开发者使用 Context 分享代码、创建可扩展的服务都会非常方便。


原作者:Sameer Ajmani 翻译:Oscar


下期预告:Go语言并发模型:使用select 。原文链接:https://talks.golang.org/2012/concurrency.slide#31

相关链接

原文链接:https://blog.golang.org/context

代码位置:https://blog.golang.org/context/

代码位置(mirror):https://github.com/oscarzhao/golang/tree/master/go_blog 包 net: https://github.com/golang/net

Google搜索API: https://developers.google.com/web-search/docs/


扫码关注微信公众号深入Go语言


 
深入Go语言 更多文章 Go语言并发模型:以并行处理MD5为例 Go语言并发模型:像Unix Pipe那样使用channel Go语言反射三定律
猜您喜欢 颤技术之巅-WE ARE TEG 浅析低延迟直播协议设计:RTP/RTCP 外面的世界很精彩 如果未来就绪企业云联盟(F.R.E.C.O.)去参加奥运会,我是说如果 说一说新员工培训