Go|Go Context 应用场景和一种错误用法
context 应用场景
Go 的 context 包,可以在我们需要在完成一项工作,会用到多个 routine (完成子任务)时,提供一种方便的在多 routine 间控制(取消、超时等)和传递一些跟任务相关的信息的编程方法。
- 一项任务会启动多个 routine 完成。
- 需要控制和同步多个 routine 的操作。
- 链式的在启动的 routine 时传递和任务相关的一些可选信息。
首先,我们定义一下
吃饭睡觉打豆豆
服务的数据结构。// DSB represents dining, sleeping and beating Doudou and
// parameters associated with such behaviours.
type DSB struct {
ateAmountint
sleptDuration time.Duration
beatSecint
}
然后提供一个 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 等)。
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 mainimport (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
)// DSB represents dining, sleeping and beating Doudou and
// parameters associated with such behaviours.
type DSB struct {
ateAmountint
sleptDuration time.Duration
beatSecint
}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:
}
}
推荐阅读
- mybatis|mybatis mapper.xml中如何根据数据库类型选择对应SQL语句
- 智能电视如何安装小程序应用(附硬核技术)
- 佛教中的“有求必应”是真的么()
- 什么是RunLoop,RunLoop有哪些使用场景
- 快讯|B站回应HR称核心用户是Loser;微博回应宕机原因;Go 1.19 正式发布|极客头条
- 愚人节不愚人
- 分布式的几件小事(九)zookeeper都有哪些使用场景
- #UIButton#背景图片的拉伸
- 新女性坐标|单身女性冻卵案判决:我们应该如何反思?
- APP研究系列之抽屉式导航的使用场景