1

Context设计模式

 3 years ago
source link: https://studygolang.com/articles/32228
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Context设计模式

危地马拉的大坑 · 1天之前 · 94 次点击 · 预计阅读时间 6 分钟 · 不到1分钟之前 开始浏览    

在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程序。


有疑问加站长微信联系(非本文作者)

280

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK