Go语言并发模式:Context包

本文翻译自Sameer Ajmani的Go Concurrency Patterns: Context

目录

Go语言Context包的讲解和使用

介绍

context

派生的contexts

例子:谷歌网页搜索

服务器程序

程序包userip

程序包google

调整代码适应Contexts

结论


 

Go语言Context包的讲解和使用

介绍

在Go服务器中,每个到达的请求都被各自对应的goroutine处理。请求的处理程序增加新的goroutine来访问后端,比如数据库和RPC服务。共同为某个请求服务的goroutine集合需要获取特定请求信息,比如终端用户的身份、认证标识和请求者的截止时间。在一个请求被取消或者过期的时候,所有相关的goroutine都需要快速退出,以便及时释放系统资源。

在谷歌,我们开发了名为context的包,它可以传递特定请求范围内的值、取消信号和截止时间到所有处理该请求的goroutine。这个包公开发布为context。这篇文章描述了如何使用这个包并提供了一个完整的工作例子。

context

context包的核心就是Context类型。

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
// 一个Context封装了截止时间、取消信号和特定请求信息,并可以通过API使用。它的方法可以安全地被
// 多个goroutine同时使用。
type Context interface {
    // Done returns a channel that is closed when this Context is canceled
    // or times out.
    // 当该Context被取消或者过时了,Done 返回一个关闭了的频道channel。
    Done() <-chan struct{}

    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    // 当Done的channel被关闭后,Err 表明为什么这个context被取消。
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    // 如果存在截止时间,Deadline 返回该Context被撤销的时间。
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    // Value 返回与key绑定的数据。如果没有相应数据则返回nil。
    Value(key interface{}) interface{}
}

Done方法返回一个频道channel,作为一个取消信号提供给与Context相关的函数:当频道关闭,这些函数需要放弃工作并返回。

Err方法返回一个错误,说明为什么context被取消。文章管线和取消细致讨论频道习语Done。

Context没有取消方法,因为同样的原因Done返回的频道是只能用于接收:接收取消信号的函数一般不是发送这个信号的函数。具体来说,一个父操作给子操作开启goroutine之后,子操作没有能力取消父操作。WithCancel提供了取消Context的一种途径。

Context能够安全地被多个goroutine同时使用。我们可以把context发送给任意多个goroutine,然后通过取消这个context来通知所有的goroutine。

Deadline方法可以帮助函数确定是否开展工作;如果剩余的时间太少,可能就不值得开始工作。代码可以使用deadline来为I/O操作设定时限。

Value允许一个Context装载特定请求的数据。这些数据必须能够安全地被多个goroutine同时使用。

派生的contexts

context提供了函数,用于从已有的context派生出新的。这些context组成了一棵树:当先辈context被取消后,所有子孙context也同时被取消。

Background是所有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中使用,作为接收到的requests的顶级context。
func Background() Context

WithCancel和WithTimeout派生出新的context值,它们可以比先辈的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.
// WithCancel返回父辈的复制品,它的Done频道随着父辈Done的关闭而关闭。
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.
// WithTimeout返回一个父辈的复制品,它的Done频道随着父辈Done的关闭而关闭或取消而取消。
// 如果存在截止时间,Context的截止时间是now+timeout和父辈截止时间两者距当前更近一个值。
// 如果计时器仍在运行,取消函数会释放它的资源。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue提供一种途径,把特定请求的数据和context连接在一起:

// WithValue returns a copy of parent whose Value method returns val for key.
// WithValue返回父辈的一个拷贝,子辈的Value方法可以根据key返回val。
func WithValue(parent Context, key interface{}, val interface{}) Context

了解context包工作机理最好的方式就是通过一个实际例子。

例子:谷歌网页搜索

我们的例子是一个HTTP服务器,处理类似于/search?q=golang&timeout=1s的URL,先把查询请求推送给谷歌网页搜索接口,然后渲染查询结果。timeout参数告诉服务器查询截止时间。

代码划分为三个包:

  • server 提供main函数和/search的处理函数。
  • userip 提供从请求中提取用户IP地址并将其与Context绑定的函数。
  • google 提供将请求发送给Google的查询函数。

服务器程序

服务器程序处理请求,返回最靠前的几个谷歌搜索结果,比如对于/search?q=golang ,它返回golang的查询结果。它注册handleSearch去处理/search终端。处理函数创建了一个名为ctx的初始Context,然后设定它在处理函数返回后取消。如果请求包含timeout URL参数,Context时间一到就会自动取消。

func handleSearch(w http.ResponseWriter, req *http.Request) {
    // ctx is the Context for this handler. Calling cancel closes the
    // ctx.Done channel, which is the cancellation signal for requests
    // started by this handler.
    var (
        ctx    context.Context
        cancel context.CancelFunc
    )
    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())
    }
    defer cancel() // Cancel ctx as soon as handleSearch returns.

处理函数从请求中提取查询任务,调用userip包提取用户IP地址。后端请求需要用户IP地址,所以handleSearch将其添加到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.FromRequest(req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    ctx = userip.NewContext(ctx, userIP)

处理函数以ctx和query作为参数调用google.Search:

    // Run the Google search and print the results.
    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程序包提供从请求提取用户IP地址并将其与Context绑定的函数。Context提供了key-value映射,这些key和value的类型都是interface{}。key的实际类型必须支持相等比较,value实际类型必须能够安全地被多个goroutine同时使用。userip这样的程序包隐藏了这个映射的细节,提供了特定Context值的强类型式访问。

为了避免键冲突,userip定义了外部无法访问的类型key,使用该类型的值作为context的键。

// The key type is unexported to prevent collisions with context keys defined in
// other packages.
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 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返回了一个包含刚得到的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
}

程序包google

google.Search函数发送了一个HTTP请求给谷歌网页搜索接口,然后解析JSON格式的结果。它接收一个Context类型参数ctx。如果ctx.Done关闭了,它就立刻返回,就算请求还在执行。

谷歌网页搜索接口请求使用搜索查询和用户IP地址作为参数:

func Search(ctx context.Context, query string) (Results, error) {
    // Prepare the Google Search API request.
    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)

    // 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-user requests.
    if userIP, ok := userip.FromContext(ctx); ok {
        q.Set("userip", userIP.String())
    }
    req.URL.RawQuery = q.Encode()

Search函数使用一个帮手函数,httpDo,来发送HTTP请求。如果ctx.Done被关闭了,它就会取消httpDo,就算请求或响应仍然在执行中。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()

        // 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
    })
    // httpDo waits for the closure we provided to return, so it's safe to
    // read results here.
    return results, err

httpDo函数执行HTTP请求,新开一个goroutine来处理响应。如果ctx.Done被取消了,在goroutine退出前,它就会取消请求。

func httpDo(ctx context.Context, req *http.Request, f func(*http.Response, 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 // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }
}

调整代码适应Contexts

许多服务器框架提供代码包和类型,用于传递特定请求数据。我们可以定义符合Context接口的新代码实现来沟通已有框架和需要Context作为参数的代码。

例如,Gorilla的 github.com/gorilla/context 程序包提供了从HTTP请求到键-值对的映射,让处理函数将数据和收到的请求联系在一起。在gorilla.go,我们提供了Context的代码实现,它的Value方法返回与Gorilla包中特定HTTP请求相对应的数据。

其他程序包也支持类似于Context的取消操作。例如,Tomb 提供了一个kill方法,通过关闭频道来传递取消信息。Tomb也提供等待那些goroutine退出的方法,类似于sync.WaitGroup。在 tomb.go中,我们提供了Context接口的另外一个实现,在先辈Context被取消或者特定Tomb被杀死后,该Context也被取消了。

结论

在谷歌,我们要求Go程序员在接收和发送请求的调用路径上,把Context参数作为每个函数的第一个参数。这让不同团队开发的Go代码能够很好地兼容。它提供了简单的过时和取消控制,确保关键信息在Go程序间准确传送,比如安全证书。

服务器框架想要适应Context,需要提供Context的实现来沟通它们的程序包和需要Context作为参数的代码。他们的客户端代码库需要能够从调用代码那里接收Context。在建立可伸缩服务的领域,通过建立一个特定请求数据和取消机制的通用接口,Context将代码共享变得更加容易。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值