美文网首页
Context设计模式

Context设计模式

作者: 危地马拉的大坑 | 来源:发表于2020-12-18 18:16 被阅读0次

    在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设计模式

          本文链接:https://www.haomeiwen.com/subject/dddfnktx.html