[翻译]GO并发模型三:基于Context编程

image.png

简书不维护了,欢迎关注我的知乎:波罗学的个人主页

原文:Context-Based Programming in Go
作者:Gigi Sayfan

在GO中,我们需要有能力管理并发运行中的goroutine,主要是指它的生命周期。那些失去控制的goroutine可能会进入某个死循环,从而导致其它等待中的goroutine死锁或运行太久。理想情况是,我们可以终止这些goroutine或使它们不不好的超时退出。

可以基于context编程。Go 1.7 引入了context包。它为我们提供了这些能力,同时我们也可以将某些变量与context关联实现信息的跨界交流与传递。

在本教程中,你将会了解到context的输入输出以及何时和如何使用它,以避免滥用。

什么情况下使用

context是一种非常好的抽象。它让你可以封装一些与核心逻辑无关的信息,比如 请求ID、认证Token和超时时间。这可以为我们带来如下的一些好处:

  • 它有效地帮助我们把核心逻辑参数与运行参数中分离开来。
  • 它为我们制定了通用的操作规则和在边界交流数据的方法。
  • 它为我们提供了一套标准的机制,在不修改函数签名的情况下传递额外信息。

Context接口

如下是Context的所有接口信息:

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done <-chan struct{}
    Err() error
    Value(key interface{})  interface{}
}

下面介绍各个方法的作用。

Deadline()

当执行完成,context就应被取消,此时Deadline()会返回相应的时间。当没有设置最后期限,Deadline返回ok == false。多次调用Deadline返回结果相同。

Done()

Done()方法返回的是一个channel,它将在工作执行完成即context应该被取消的时候被关闭。连续调用Done()返回的结果相同。

  • context.WithCancel()返回cancel函数,当调用它时,Done会被关闭;
  • context.WithDeadline()设置过期时间,当过期后,Done会被关闭;
  • context.WithTimeout()设置超时时间,当超时后,Done会被关闭;

可以在select语句中使用Done:

func Stream(ctx context.Context, out chan<- Value) error {
    for {
        v, err := DoSomething(ctx)
        if err != nil {
            return err
        }

        select {
        case <-ctx.Done():
            return ctx.Err()
        case out <- v:
        }
    }
}

可以读下这篇文章Go并发模型:Pipeline和Cancellation,介绍很多关于如何使用Done取消context的例子。

Err()

只要Done是打开状态,Err()返回nil。如果context被取消,它返回Canceled error。如果context到期或超时,它返回DeadlineExceeded error。Done被关闭后,多次调用Err()返回结果相同。下面是一些定义:

// Canceled is the error returned by Context.Err when the context is canceled.
var Canceled = errors.New("context canceled")

// DeadlineExceeded is the error returned by Context.Err when the context's deadline passes.
var DeadlineExceeded error = deadlineExceededError{}

Value()

Value()通过key去调用与context关联的value,如果context中与指定key对应的值,返回nil。多次以相同key调用Value()返回结果相同。

Context中的Value仅仅用于在请求范围内不同程序和接口的数据转化,不可用于其他的参数传递。

在Context,一个key代表一个具体的值。那些希望用Context存储数据值的函数通常会在全局分配一个变量key,并用这个key作为参数调用context.WithValue和context.Value()。key支持任何类型。

Context的作用域

Contexts有作用范围。你可以从已有的context作用域延伸出新的作用域。父级不能访问衍生的作用域的数据,不过下级是可以访问父级作用域数据的。

Contexts是层级结构。你可以通过context.Background()或context.TODO()创建contexts。无论何时你调用WithCancel、WithDeadline或WithTimeout,都会得到出新的context,同时会返回一个cancel函数。最重要的是当父级的context被取消,所有的子级也将取消。

你应该在main、init和tests中使用context.Background()。如果不知道该使用什么context,可以通过context.TODO()产生context。

注意,Background和TODO生成的context是不可取消的。

过期、超时和取消

如你所知,WithDeadline() 和 WithTimeout() 创建的contexts将会自动取消,而WithCancel() 创建的context必须通过cancel()明确指定何时取消。其实,它们都会返回一个cancel函数,所以既没有超时/过期,你依然可以通过cancel取消衍生的context。

让我们看个例子。首先,contextDemo函数有两个参数,分别是name和context。它在一个无限循环中运行,不停的在控制台打印name和deadline(如果有的话)。然后sleep一秒。

package main

import (
    "fmt"
    "context"
    "time"
)

func contextDemo(name string, ctx context.Context) {
    for {
        if ok {
            fmt.Println(name, "will expire at:", deadline)
        } else {
            fmt.Println(name, "has no deadline")
        }
        time.Sleep(time.Second)
    }
}

主函数创建了三个contexts:

  • 三秒超时的timeoutContext;
  • 没有过期时间的cancelContext;
  • 由cancelContext产生的从现在开始4小时过期的deadlineContext;

然后,启动三个contextDemo的goroutine。它们并发执行且每秒打印一次message。

主函数通过读取timeoutContext的Done()来实现等待goroutine超时退出。一但三秒超时,main函数就调用cancelFunc取消cancelContext中的goroutine,同时cancelContext衍生出来的4小时过期的deadlineContext的goroutine也将退出。

func main() {
    timeout := 3 * time.Second
    deadline := time.Now().Add(4 * time.Hour)
    timeOutContext, _ := context.WithTimeout(
        context.Background(), timeout)
    cancelContext, cancelFunc := context.withCancel(
        context.Background())
    deadlineContext, _ := context.WithDeadline(
        cancelContext, deadline)
        
    go contextDemo("[timeoutContext]", timeOutContext)
    go contextDemo("[cancelContext]", cancelContext)
    go contextDemo("[deadlineContext]", deadlineContext)
 
    // Wait for the timeout to expire
    <- timeOutContext.Done()
 
    // This will cancel the deadline context as well as its
    // child - the cancelContext
    fmt.Println("Cancelling the cancel context...")
    cancelFunc()
 
    <- cancelContext.Done()
    fmt.Println("The cancel context has been cancelled...")
 
    // Wait for both contexts to be cancelled
    <- deadlineContext.Done()
    fmt.Println("The deadline context has been cancelled...")       
}

下面是输出结果:

[cancelContext] has no deadline
[deadlineContext] will expire at: 2017-07-29 09:06:02.34260363
[timeoutContext] will expire at: 2017-07-29 05:06:05.342603759
[cancelContext] has no deadline
[timeoutContext] will expire at: 2017-07-29 05:06:05.342603759
[deadlineContext] will expire at: 2017-07-29 09:06:02.34260363
[cancelContext] has no deadline
[timeoutContext] will expire at: 2017-07-29 05:06:05.342603759
[deadlineContext] will expire at: 2017-07-29 09:06:02.34260363
Cancelling the cancel context...
The cancel context has been cancelled...
The deadline context has been cancelled...

输出结果不变。接下来是最佳实践章节,将介绍一些指导原则,以便于我们恰当地使用context数据传递。

最佳实践

围绕context数据传递的几个最佳实践:

  • 避免在context中传递函数参数;
  • 在全局变量中为context中的数据分配一个对应key;
  • 包中应该为key定义一个不可导出的类型,以防止发生冲突;
  • 包中定义的key应该为其在context存储的数据提供类型安全访问方法;

HTTP请求的Context

context的常用场景之一就是在HTTP请求间传递信息。这些信息可能包含请求ID、认证证书等。在GO1.7,标准库net/http利用了context的优势,并且已经标准化,直接在request中加入了对context的支持。

func (r *Request) Context() context.Context
func (r *Request) WithContext(ctx context.Context) *Request

现在,我们可以使用一种标准方式把从headers中获取到的requestId传递到最终的处理函数。WithRequestID() 处理函数从"X-Request-ID"头部导出requestID并从正在使用的context中衍生出一个带有requestID的context。然后把它传递给调用链的下一个处理函数。公共函数GetRequestID()为处理函数提供了访问RequestID的途径,包括定义在其他包的处理函数。

const requestIDKey int = 0
 
func WithRequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(
        func(rw http.ResponseWriter, req *http.Request) {
            // Extract request ID from request header
            reqID := req.Header.Get("X-Request-ID")
            // Create new context from request context with 
            // the request ID
            ctx := context.WithValue(
                req.Context(), requestIDKey, reqID)
            // Create new request with the new context
            req = req.WithContext(ctx)
            // Let the next handler in the chain take over.
            next.ServeHTTP(rw, req)
        }
    )
}
 
func GetRequestID(ctx context.Context) string {
    ctx.Value(requestIDKey).(string)
}

func Handle(rw http.ResponseWriter, req *http.Request) {
    reqID := GetRequestID(req.Context())
    ...
}

func main() {
    handler := WithRequestID(http.HandlerFunc(Handle))
    http.ListenAndServe("/", handler)
}

总结

基于Context的编程为我们提供了一套标准和良好支持的方法,它解决了两个常见的问题:goroutine的生命周期管理和信息传递。

以最佳实践为准,在合适的场景下使用contexts,你的编码能力将会大幅提升。

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

推荐阅读更多精彩内容