Go Concurrency Patterns: Context

Go Concurrency Patterns: Context

原文地址:https://blog.golang.org/context

Introduction

在 Go 语言实现的服务器上,我们总是使用 goroutine 来处理与客户端建立的连接, 给每个连接分配一个独立的 goroutine. 在请求的 handler 中也通常会再开启额外的 goroutine 来访问后台的各种服务,比如 数据库操作,访问 RFC 服务。 在这种情况下,一个客户端连接往往会对应一组 goroutine,这些 goroutine 在运行过程中往往还需要用到请求相关的数据,比如,用户id,授权token,请求的 deadline 等. 当一个请求超时,或者被cancel之后,这些 goroutine 就需要被尽快的释放出来,以便为其他请求服务.

为此 Google 开发了一个 context 包,使用这个包使得在一组 共routine 中传递请求相关的参数,cancel 信号,deadline 变得非常容易. 本文将描述如何使用 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.
type Context interface {
	// Done returns a channel that is closed when this Context is canceled 
	// or times out.
    Done() <-chan struct{}
    
    // Err indicates why this context was canceled, after the Done channel
    // is closed.
    Err() error

	// Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

	 // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}

Done

Deadline

Value 允许 context 携带请求相关的数据,必须确保在多个 goroutine 中访问中访问这些数据是安全的.

Derived contexts

Context 包提供了从一个现有的 context 派生出其他 context 的方法. 这些 context 共同构成一颗 context 树: 当一个 context 被 cancel 的时候,从这个树派生出来的 context 也会被 cancel

Background 是任何 context 树的根, 它不能被 cancel:

// 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.
func Background() Context

WithCancelWithTimeout 用于从当前 context 派生新的 context. 与客户端请求相关的 context 通常会被 cancel 当处理请求的 handler 退出的时候. WithCancel 对于处理多余的请求非常有用. WithTimeout 通常用来给一个请求设置一个超时时间.

// WithCancel returns a copy of parent whose Done channel is closed as soon as
// parent.Done is closed or cancel is called.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a 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.
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
Example: Google Web Search

我们的示例是一个用户处理像 /search?q=golang&timeout=1s 这种请求的 HTTP 服务器.
HTTP 服务器将查询 “golang” 的请求转发到 Google Web Search API, 并返回请求结果. timeout 参数告诉服务器在多久之后 cancel 这个请求.

代码被组织在三个 package 中:

  • server: 提供 main 方法和对应与 /search 的 hander
  • userip: 提供了从用户请求解析用户 IP 地址和 将 IP 地址与 context 绑定的方法
  • google: 提供了 Search 方法的实现
server
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.
    
    // 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)

    // 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
// 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 解析用户 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)
	}
	return ip, nil
}

// NewContext 返回一个携带有用户 IP 地址的 context 实例
func NewContext(ctx context.Context, userIP net.IP) context.Context {
    return context.WithValue(ctx, userIPKey, 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
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()

	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
}

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
    }
}
Adapting code for Contexts

许多服务器框架都会提供 package 和 types 来携带请求数据. 在这种情况下,我们可以通过实现 Context 接口来将已有框架的类型转化为 context 类型.

举个例子, Gorilla package 通过提供了保存请求参数的 map 来携带请求数据. 在 Gorilla 中,我们可以提供一个实现 context 接口的自定义 context 类型,该类型重写 Value 方法,该方法返回 Gorilla 中保存的 map.

Conclusion

在谷歌中,我们要求工程师总是将 context 参数作为传入和传出请求之间调用路径上的每个函数的第一个参数类传递. 这使得许多不同团队开发的 Go 代码具有很好的互操作性。它提供了对超时和 cancel 的简单控制,并确保数据正确传输。

希望基于 context 的服务器框架应该提供自己的 context 实现,以便框架自身的 context 与标准 go context 的兼容. 因此,它们的客户端代码通常都接受一个来自调用者的 context 对象.

Related articles

END!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值