How to correctly use package context by Jack Lindamood at Golang UK Conf. 2017html
视频:www.youtube.com/watch?v=-_B… 博文:medium.com/@cep21/how-…git
若是进一步考虑。github
这是正常的方式,可是若是 RPC 2 调用失败了会发生什么?golang
RPC 2 失败后,若是没有 Context 的存在,那么咱们可能依旧会等全部的 RPC 执行完毕,可是因为 RPC 2 败了,因此其实其它的 RPC 结果意义不大了,咱们依旧须要给用户返回错误。所以咱们白白的浪费了 10ms,彻底不必去等待其它 RPC 执行完毕。数据库
那若是咱们在 RPC 2 失败后,就直接给用户返回失败呢?安全
因此理想状态应该是如上图,当 RPC 2 出错后,除了返回用户错误信息外,咱们也应该有某种方式能够通知 RPC 3 和 RPC 4,让他们也中止运行,再也不浪费资源。bash
因此解决方案就是:服务器
那干脆让咱们把变量也扔那吧。😈微信
context.Context:并发
完整代码:play.golang.org/p/ddpofBV1Q…
package main
func tree() {
ctx1 := context.Background()
ctx2, _ := context.WithCancel(ctx1)
ctx3, _ := context.WithTimeout(ctx2, time.Second * 5)
ctx4, _ := context.WithTimeout(ctx3, time.Second * 3)
ctx5, _ := context.WithTimeout(ctx3, time.Second * 6)
ctx6 := context.WithValue(ctx5, "userID", 12)
}
复制代码
若是这样构成的 Context 链,其形以下图:
当 5秒钟 超时到达时:
基本上是两类操做:
type Context interface {
// 啥时候退出
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
// 设置变量
Value(key interface{}) interface{}
}
复制代码
ctx, cancel := context.WithTimeout(parentCtx, time.Second * 2)
defer cancel()
复制代码
当你再也不关心接下来获取的结果的时候,有可能会 Cancel 一个 Context?
以 golang.org/x/sync/errgroup 为例,errgroup 使用 Context 来提供 RPC 的终止行为。
type Group struct {
cancel func()
wg sync.WaitGroup
errOnce sync.Once
err error
}
复制代码
建立一个 group 和 context:
func WithContext(ctx context.Context) (*Group, context.Context) {
ctx, cancel := context.WithCancel(ctx)
return &Group{cancel: cancel}, ctx
}
复制代码
这样就返回了一个能够被提早 cancel 的 group。
而调用的时候,并非直接调用 go func(),而是调用 Go(),将函数做为参数传进去,用高阶函数的形式来调用,其内部才是 go func() 开启 goroutine。
func (g *Group) Go(f func() error) {
g.wg.Add(1)
go func() {
defer g.wg.Done()
if err := f(); err != nil {
g.errOnce.Do(func() {
g.err = err
if g.cancel != nil {
g.cancel()
}
})
}
}()
}
复制代码
当给入函数 f 返回错误,则使用 sync.Once 来 cancel context,而错误被保存于 g.err 之中,在随后的 Wait() 函数中返回。
func (g *Group) Wait() error {
g.wg.Wait()
if g.cancel != nil {
g.cancel()
}
return g.err
}
复制代码
注意:这里在 Wait() 结束后,调用了一次 cancel()。
package main
func DoTwoRequestsAtOnce(ctx context.Context) error {
eg, egCtx := errgroup.WithContext(ctx)
var resp1, resp2 *http.Response
f := func(loc string, respIn **http.Response) func() error {
return func() error {
reqCtx, cancel := context.WithTimeout(egCtx, time.Second)
defer cancel()
req, _ := http.NewRequest("GET", loc, nil)
var err error
*respIn, err = http.DefaultClient.Do(req.WithContext(reqCtx))
if err == nil && (*respIn).StatusCode >= 500 {
return errors.New("unexpected!")
}
return err
}
}
eg.Go(f("http://localhost:8080/fast_request", &resp1))
eg.Go(f("http://localhost:8080/slow_request", &resp2))
return eg.Wait()
}
复制代码
在这个例子中,同时发起了两个 RPC 调用,当任何一个调用超时或者出错后,会终止另外一个 RPC 调用。这里就是利用前面讲到的 errgroup 来实现的,应对有不少并不是请求,并须要集中处理超时、出错终止其它并发任务的时候,这个 pattern 使用起来很方便。
胶带(duct tape) 几乎能够修任何东西,从破箱子,到人的伤口,到汽车引擎,甚至到NASA登月任务中的阿波罗13号飞船(Yeah! True Story)。因此在西方文化里,胶带是个“万能”的东西。在中文里,恐怕万金油是更合适的对应词汇,从头疼、脑热,感冒发烧,到跌打损伤几乎无所不治。
固然,治标不治本,这点东西方文化中的潜台词都是同样的。这里说起的 context.Value 对于 API 而言,就是这类性质的东西,啥均可以干,可是治标不治本。
package context
type valueCtx struct {
Context
key, val interface{}
}
func WithValue(parent Context, key, val interface{}) Context {
// ...
return &valueCtx{parent, key, val}
}
func (c *valueCtx) Value(key interface{}) interface{} {
if c.key == key {
return c.val
}
return c.Context.Value(key)
}
复制代码
能够看到,WithValue() 实际上就是在 Context 树形结构中,增长一个节点罢了。
Context 是 immutable 的。
为了防止树形结构中出现重复的键,建议约束键的空间。好比使用私有类型,而后用 GetXxx() 和 WithXxxx() 来操做私有实体。
type privateCtxType string
var (
reqID = privateCtxType("req-id")
)
func GetRequestID(ctx context.Context) (int, bool) {
id, exists := ctx.Value(reqID).(int)
return id, exists
}
func WithRequestID(ctx context.Context, reqid int) context.Context {
return context.WithValue(ctx, reqID, reqid)
}
复制代码
这里使用 WithXxx 而不是 SetXxx 也是由于 Context 其实是 immutable 的,因此不是修改 Context 里某个值,而是产生新的 Context 带某个值。
再屡次的强调 Context.Value 是 immutable 的也不过度。
func Add(ctx context.Context) int {
return ctx.Value("first").(int) + ctx.Value("second").(int)
}
复制代码
曾经看到过一个 API,就是这种形式:
func IsAdminUser(ctx context.Context) bool {
userID := GetUser(ctx)
return authSingleton.IsAdmin(userID)
}
复制代码
这里API实现内部从 context 中取得 UserID,而后再进行权限判断。可是从函数签名看,则彻底没法理解这个函数具体须要什么、以及作什么。
代码要以可读性为优先设计考虑。
别人拿到一个代码,通常不是掉进函数实现细节里去一行行的读代码,而是会先浏览一下函数接口。因此清晰的函数接口设计,会更加利于别人(或者是几个月后的你本身)理解这段代码。
一个良好的 API 设计,应该从函数签名就清晰的理解函数的逻辑。若是咱们将上面的接口改成:
func IsAdminUser(ctx context.Context, userID string, authenticator auth.Service) bool
复制代码
咱们从这个函数签名就能够清楚的知道:
全部这些信息,都是从函数签名获得的,而无需打开函数实现一行行去看。
如今知道 Context.Value 会让接口定义更加模糊,彷佛不该该使用。那么又回到了原来的问题,到底什么能够放到 Context.Value 里去?换个角度去想,什么不是衍生于 Request?
package main
func trace(req *http.Request, c *http.Client) {
trace := &httptrace.ClientTrace{
GotConn: func(connInfo httptrace.GotConnInfo) {
fmt.Println("Got Conn")
},
ConnectStart: func(network, addr string) {
fmt.Println("Dial Start")
},
ConnectDone: func(network, addr string, err error) {
fmt.Println("Dial done")
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
c.Do(req)
}
复制代码
package http
func (req *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error) {
// ...
trace := httptrace.ContextClientTrace(req.Context())
// ...
if trace != nil && trace.WroteHeaders != nil {
trace.WroteHeaders()
}
}
复制代码
package main
import "github.com/golang/oauth2"
func oauth() {
c := &http.Client{Transport: &mockTransport{}}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, c)
conf := &oauth2.Config{ /* ... */ }
conf.Exchange(ctx, "code")
}
复制代码
context.Value 并无让你的 API 更简洁,那是假象,相反,它让你的 API 定义更加模糊。
以前说过长时间、可阻塞的操做都用 Context,数据库操做也是如此。不过对于超时 Cancel 操做来讲,通常不会对写操做进行 cancel;可是对于读操做,通常会有 Cancel 操做。
我的微信公众号:
我的github:
我的博客: