Context设计模式

在Go中,每个请求都会在各自所在的goroutine中运行。Context包可以方便地在各个goroutine之间传值和发送取消[1]、达到*最后期限*[2]信号。

Context的接口定义

// Context携带着deadline和取消信号,和request-scoped的值跨域API的界限在goroutine之间传递,
// 且保证是同步安全的
type Context interface {
    // 当此Context取消或者超时,Done返回一个channel
    Done() <-chan struct{}

    // 此context被取消的错误原因
    Err() error

    // Deadline返回什么时候Context由于超时会被取消
    Deadline() (deadline time.Time, ok bool)

    // request-scoped需要共享的值
    Value(key interface{}) interface{}
}

获取Context

Context值是以树状结构呈现的,如果Context被取消,那么他的子Context也会被取消。

Background是Context树的根,它不能被取消:

// Background返回一个空Context。它不允许被取消,没有最后期限,没有值。
// Background被用在main,init和tests的地方,作为进来的请求的最顶级Context。
func Background() Context

WithCancelWithTimeout返回的Conetxt是可以被取消的。此Context关联的请求处理完成返回时,就会被取消。WithCancel多用于关联冗余请求,WithTimeout多用于关联后台服务要求设置超时的请求。

// WithCancel返回一个parent Context的副本,当parent的Done Channel被关闭,或者cancel被调用,
// 那么它的Done Channel也会被关闭
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

// A CancelFunc cancels a Context.
type CancelFunc func()

// WithTimeout返回一个parent Context的副本,当parent的Done Channel被关闭,或者cancel被调用,
// 或者超时时间已到,那么它的Done Channel也会被关闭。
// 此新Context的最后期限必须比now + timeout要早。如果timer仍然在运行,那么cancel方法会释放资源。
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

WithValue提供了一个关联request-scoped值的方法。

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

下面提供一个最佳实践。

例子:Google Web Search

我们的例子是处理一个http请求,URL是/search?q=golang&timeout=1s表示查询golang关键字,timeout参数表示请求的超时时间,最后调用Google Web Search API获取数据并渲染结果。

代码分成三个包:

  • server提供main方法和处理/search请求
  • userip提供抽取IP地址,并把它关联到Context的方法
  • google提供Search方法,调用google的api
server代码

此处理器创建第一个Context,称为ctx;同时把它设置为当处理返回后被取消。如果URL包含timeout参数,此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 {
        // 此请求有超时入参,所以此context超时后会被自动取消
        ctx, cancel = context.WithTimeout(context.Background(), timeout)
    } else {
        ctx, cancel = context.WithCancel(context.Background())
    }
    // handleSearch返回后发送取消信号,取消ctx
    defer cancel()

此处理器在请求里抽取出客户端IP地址,然后调用userip包的方法。此客户端IP地址在后面的请求中会用到,所以handleSearch把它附在ctx里:

    // 获取入参q的值并校验
    query := req.FormValue("q")
    if query == "" {
        http.Error(w, "no query", http.StatusBadRequest)
        return
    }

        // 使用其他包的代码存储客户端IP地址
    userIP, err := userip.FromRequest(req)
    if err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    ctx = userip.NewContext(ctx, userIP)

此处理器调用google.Search方法,并带上了ctxquery

    // 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地址和把IP地址关联到Context的功能。Context提供key-value映射关系存储。

为了避免key发生碰撞,userip定义一个私有类型key,使用此类型的值作为Context的key:

// 此key类型是私有的,避免与其他包所定义的key发生碰撞
type key int

// userIPkey是客户端IP地址的Context key。如果此包定义了其他的Context key,他们需要使用其他整数值
const userIPKey key = 0

FromRequesthttp.Request抽取出客户端IP地址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) {
    // 如果不存在此key对应的value,ctx.Value返回nil
    userIP, ok := ctx.Value(userIPKey).(net.IP)
    return userIP, ok
}
google包

google.Search 方法创建一个HTTP请求 Google Web Search API 接口,并把结果解析成JSON结构。它接受一个Context类型的参数ctx,并且当请求没有响应导致 ctx.Done 被关闭时会马上返回。

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()
    // 参数1
    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 {
        // 参数2
        q.Set("userip", userIP.String())
    }
    req.URL.RawQuery = q.Encode()

Search 使用了一个辅助方法, httpDo,发起HTTP请求,如果 ctx.Done 在处理请求或响应时关闭了HTTP请求,则将其取消。Search 传递了一个闭包方法来处理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 方法会在一个新的goroutine中运行HTTP请求和处理其响应。如果ctx.Done在goroutine退出之前已关闭,它将取消请求:

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 // Wait for f to return.
        return ctx.Err()
    case err := <-c:
        return err
    }
}

总结

在Google,我们要求Go程序员必须把Context作为第一入参传递到每个有传入传出的请求函数中。这样可以使不同的Go开发团队很好地进行互动操作。他提供对超时和取消的简单控制,并保证安全凭证之类的关键值正确地传导到Go程序。

希望基于Context开发的服务框架需要提供实现,让你的包和那些需要一个Context参数的包建立桥梁联系。客户端库会接受一个来自调用方的Context。通过为request-scoped和取消建立一个通用接口,Context可以让包开发者更容易地为可伸缩服务分享代码。

你可能感兴趣的:(Context设计模式)