关于go:Go-Context-应用场景和一种错误用法

61次阅读

共计 5658 个字符,预计需要花费 15 分钟才能阅读完成。

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:
    }
}

正文完
 0