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可以让包开发者更容易地为可伸缩服务分享代码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容

  • 在GO中,我们需要有能力管理并发运行中的goroutine,主要是指它的生命周期。那些失去控制的goroutine...
    浩轩01阅读 273评论 0 0
  • 什么是context 从go1.7开始,golang.org/x/net/context包正式作为context包...
    天唯阅读 12,584评论 3 26
  • context context包定义了上下文类型,该类型在API边界之间以及进程之间传递截止日期,取消信号和其他请...
    DevilRoshan阅读 369评论 0 0
  • 控制并发有两种经典的方式,一种是WaitGroup,另外一种就是Context,今天我就谈谈Context。 什么...
    豆瓣奶茶阅读 5,052评论 4 13
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,495评论 16 22