Go Context 应用场景和一种错误用法

context 应用场景

Go 的 context 包,可以在我们需要在完成一项工作,会用到多个 routine (完成子任务)时,提供一种方便的在多 routine 间控制(取消、超时等)和传递一些跟任务相关的信息的编程方法。

  • 一项任务会启动多个 routine 完成。
  • 需要控制和同步多个 routine 的操作。
  • 链式的在启动的 routine 时传递和任务相关的一些可选信息。

举一个例子,这里我们提供了一个服务,每一次调用,它提供三个功能:吃饭、睡觉和打豆豆。调用者通过设置各种参数控制3个操作的时间或次数,同时开始执行这些操作,并且可以在执行过程中随时终止。

首先,我们定义一下吃饭睡觉打豆豆服务的数据结构。

// DSB represents dining, sleeping and beating Doudou and
// parameters associated with such behaviours.
type DSB struct {
 ateAmount int
 sleptDuration time.Duration
 beatSec int
}

然后提供一个 Do 函数执行我们设置的操作。

func (dsb *DSB) Do(ctx context.Context) {
 go dsb.Dining(ctx)
 go dsb.Sleep(ctx)
 
 // Limit beating for 3 seconds to prevent a serious hurt on Doudou.
 beatCtx, cancelF := context.WithTimeout(ctx, time.Second*3)
 defer cancelF()
 go dsb.BeatDoudou(beatCtx)
 // ...
}

具体的执行某一个操作的方法大概是这样的:会每隔1秒执行一次,直至完成或者被 cancel。

func (dsb *DSB) BeatDoudou(ctx context.Context) {
 for i := 0; i < dsb.beatSec; i++ {
 select {
 case <-ctx.Done():
 fmt.Println("Beating cancelled.")
 return
 case <-time.After(time.Second * 1):
 fmt.Printf("Beat Doudou [%d/%d].\n", i+1, dsb.beatSec)
 }
 }
}

初始化参数,注意打豆豆的时间会因为我们之前的context.WithTimeout(ctx, time.Second*3)被强制设置为最多3秒。

dsb := DSB{
 ateAmount: 5,
 sleptDuration: time.Second * 3,
 beatSec: 100,
}
ctx, cancel := context.WithCancel(context.Background())

代码详见附件。如果顺利的执行完,大概是这样的:

Ate: [0/5].
Beat Doudou [1/100].
Beat Doudou [2/100].
Ate: [1/5].
Beating cancelled.
Have a nice sleep.
Ate: [2/5].
Ate: [3/5].
Ate: [4/5].
Dining completed.
quit

但是如果中途我们发送尝试终止(发送 SIGINT)的话,会使用 ctx把未执行 完成的行为终止掉。

Ate: [0/5].
Beat Doudou [1/100].
Beat Doudou [2/100].
Ate: [1/5].
^CCancel by user.
Dining cancelled, ate: [2/5].
Sleeping cancelled, slept: 2.95261025s.
Beating cancelled.
quit

推荐的使用方式

  • 规则1: 尽量小的 scope。
    每个请求类似时候 用法通过简单的,每次调用时传入 context 可以明确的定义它对应的调用的取消、截止以及 metadata 的含义,也清晰地做了边界隔离。要把 context 的 scope 做的越小越好。
  • 规则2: 不把 context.Value 当做通用的动态(可选)参数传递信息。
    在 context 中包含的信息,只能够是用于描述请求(任务) 相关的,需要在 goroutine 或者 API 之间传递(共享)的数据。
    通常来说,这种信息可以是 id 类型的(例如玩家id、请求 id等)或者在一个请求或者任务生存期相关的(例如 ip、授权 token 等)。

我们需要澄清,context 的核心功能是==跨 goroutine 操作==。
Go 里面的 context 是一个非常特别的概念,它在别的语言中没有等价对象。同时,context 兼具「控制」和「动态参数传递」的特性又使得它非常容易被误用。

Cancel 操作的规则:调用 WithCancel 生成新的 context 拷贝的 routine 可以 Cancel 它的子 routine(通过调用 WithCancel 返回的 cancel 函数),但是一个子 routine 是不能够通过调用例如 ctx.Cancel()去影响 parsent routine 里面的行为。

错误的用法

不要把 context.Context 保存到其他的数据结构里。

参考 Contexts and structs

如果把 context 作为成员变量在某一个 struct 中,并且在不同的方法中使用,就混淆了作用域和生存周期。于是使用者无法给出每一次 Cancel 或者 Deadline 的具体意义。对于每一个 context,我们一定要给他一个非常明确的作用域和生存周期的定义。

在下面的这个例子里面,Server 上面的 ctx 没有明确的意义。

  • 它是用来描述定义 启动(Serve) 服务器的生命周期的?
  • 它是对 callA/callB 引入的 goroutine 的执行的控制?
  • 它应该在那个地方初始化?

这些都是问题。

type Server struct {
 ctx context.Context
 // ...
}
func (s *Server) Serve() {
 for {
 select {
 case <-s.ctx.Done():
 // ...
 }
 }
}
func (s *Server) callA() {
 newCtx, cancelF := WithCancel(s.ctx)
 go s.someCall(newCtx)
 // ...
}
func (s *Server) callB() {
 // do something
 select {
 case <-s.ctx.Done():
 // ...
 case <-time.After(time.Second * 10):
 // ...
 }
}

例外

有一种允许你把 context 以成员变量的方式使用的场景:兼容旧代码。

// 原来的方法
func (c *Client) Do(req *Request) (*Response, error)
// 正确的方法定义
func (c *Client) Do(ctx context.Context, req *Request) (*Response, error)
// 为了保持兼容性,原来的方法在不改变参数定义的情况下,把 context 放到 Request 上。
type Request struct {
 ctx context.Context
 // ...
}
// 创建 Request 时加一个 context 上去。
func NewRequest(method, url string, body io.Reader) (*Request, error) {
 return NewRequestWithContext(context.Background(), method, url, body)
}

在上面的代码中,一个 Request 的请求的尝尽,是非常契合 context 的设计目的的。因此,在 Client.Do 里面传递 context.Context 是非常符合 Go 的规范且优雅的。

看是考虑到net/http等标准库已经在大范围的使用,粗暴的改动接口也是不可取的,因此在net/http/request.go这个文件的实现中,就直接把 ctx 挂在 Request 对象上了。

type Request struct {
 // ctx is either the client or server context. It should only
 // be modified via copying the whole Request using WithContext.
 // It is unexported to prevent people from using Context wrong
 // and mutating the contexts held by callers of the same request.
 ctx context.Context

在 context 出现前的取消操作

那么,在没有 context 的时候,又是如何实现类似取消操作的呢?
我们可以在 Go 1.3 的源码中瞥见:

// go1.3.3 代码: go/src/net/http/request.go 
 // Cancel is an optional channel whose closure indicates that the client
 // request should be regarded as canceled. Not all implementations of
 // RoundTripper may support Cancel.
 //
 // For server requests, this field is not applicable.
 Cancel <-chan struct{}

使用的时候,把你自己的 chan 设置到 Cancel 字段,并且在你想要 Cancel 的时候 close 那个 chan。

ch := make(chan struct{})
req.Cancel = ch
go func() {
 time.Sleep(1 * time.Second)
 close(ch)
}()
res, err := c.Do(req)

这种用法看起来有些诡异,我也没有看到过人这么使用过。

额外

如果 对一个已经设置了 timeout A 时间的 ctx 再次调用 context.WithTimeout(ctx, timeoutB),得到的 ctx 会在什么时候超时呢?
答案: timeout A 和 timeout B 中先超时的那个。

附:打豆豆代码
package main
import (
 "context"
 "fmt"
 "os"
 "os/signal"
 "sync"
 "syscall"
 "time"
)
// DSB represents dining, sleeping and beating Doudou and
// parameters associated with such behaviours.
type DSB struct {
 ateAmount int
 sleptDuration time.Duration
 beatSec int
}
func (dsb *DSB) Do(ctx context.Context) {
 wg := sync.WaitGroup{}
 wg.Add(3)
 go func() {
 dsb.Dining(ctx)
 wg.Done()
 }()
 go func() {
 dsb.Sleep(ctx)
 wg.Done()
 }()
 // Limit beating for 3 seconds to prevent a serious hurt on Doudou.
 beatCtx, cancelF := context.WithTimeout(ctx, time.Second*3)
 defer cancelF()
 go func() {
 dsb.BeatDoudou(beatCtx)
 wg.Done()
 }()
 wg.Wait()
 fmt.Println("quit")
}
func (dsb *DSB) Sleep(ctx context.Context) {
 begin := time.Now()
 select {
 case <-ctx.Done():
 fmt.Printf("Sleeping cancelled, slept: %v.\n", time.Since(begin))
 return
 case <-time.After(dsb.sleptDuration):
 }
 fmt.Printf("Have a nice sleep.\n")
}
func (dsb *DSB) Dining(ctx context.Context) {
 for i := 0; i < dsb.ateAmount; i++ {
 select {
 case <-ctx.Done():
 fmt.Printf("Dining cancelled, ate: [%d/%d].\n", i, dsb.ateAmount)
 return
 case <-time.After(time.Second * 1):
 fmt.Printf("Ate: [%d/%d].\n", i, dsb.ateAmount)
 }
 }
 fmt.Println("Dining completed.")
}
func (dsb *DSB) BeatDoudou(ctx context.Context) {
 for i := 0; i < dsb.beatSec; i++ {
 select {
 case <-ctx.Done():
 fmt.Println("Beating cancelled.")
 return
 case <-time.After(time.Second * 1):
 fmt.Printf("Beat Doudou [%d/%d].\n", i+1, dsb.beatSec)
 }
 }
}
func main() {
 dsb := DSB{
 ateAmount: 5,
 sleptDuration: time.Second * 3,
 beatSec: 100,
 }
 ctx, cancel := context.WithCancel(context.Background())
 done := make(chan struct{}, 1)
 go func() {
 dsb.Do(ctx)
 done <- struct{}{}
 }()
 sig := make(chan os.Signal, 1)
 signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
 select {
 case <-sig:
 fmt.Println("Cancel by user.")
 cancel()
 <-done
 case <-done:
 }
}
作者:赞原文地址:https://segmentfault.com/a/1190000042265969

%s 个评论

要回复文章请先登录注册