context 学习

比如,服务端接收到客户端的 HTTP 请求之后,可以把客户端的 IP 地址和端口、客户端的 身份信息、请求接收的时间、Trace ID 等信息放入到上下文中,这个上下文可以在后端的 方法调用中传递,后端的业务方法除了利用正常的参数做一些业务处理(如订单处理)之 外,还可以从上下文读取到消息请求的时间、Trace ID 等信息,把服务处理的时间推送到 Trace 服务中。Trace 服务可以把同一 Trace ID 的不同方法的调用顺序和调用时间展示成 流程图,方便跟踪。 不过,Go 标准库中的 Context 功能还不止于此,它还提供了超时(Timeout)和取消 (Cancel)的机制,下面就让我一一道来。

Go 的开发者也注意到了“关于 Context,存在一些争议”这件事儿,所以,Go 核心开发 者 Ian Lance Taylor 专门开了一个 issue 28342,用来记录当前的 Context 的问题:

Context 包名导致使用的时候重复 ctx context.Context; Context.WithValue 可以接受任何类型的值,非类型安全; Context 包名容易误导人,实际上,Context 最主要的功能是取消 goroutine 的执行; Context 漫天飞,函数污染。

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

img

context的使用

首先使用context实现文章开头done channel的例子来示范一下如何更优雅实现协程间取消信号的同步:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func main() {
    messages := make(chan int, 10)

    // producer
    for i := 0; i < 10; i++ {
        messages <- i
    }

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

    // consumer
    go func(ctx context.Context) {
        ticker := time.NewTicker(1 * time.Second)
        for _ = range ticker.C {
            select {
            case <-ctx.Done():
                fmt.Println("child process interrupt...")
                return
            default:
                fmt.Printf("send message: %d\n", <-messages)
            }
        }
    }(ctx)

    defer close(messages)
    defer cancel()

    select {
    case <-ctx.Done():
        time.Sleep(1 * time.Second)
        fmt.Println("main process exit!")
    }
}

这个例子中,只要让子线程监听主线程传入的ctx,一旦ctx.Done()返回空channel,子线程即可取消执行任务。但这个例子还无法展现context的传递取消信息的强大优势。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
    "context"
    "log"
    "os"
    "time"
)

var logg *log.Logger

func someHandler() {
    ctx, cancel := context.WithCancel(context.Background())
    go doStuff(ctx)

//10秒后取消doStuff
    time.Sleep(10 * time.Second)
    // 取消 ctx ,用于超时控制
    cancel()

}

//每1秒work一下,同时会判断ctx是否被取消了,如果是就退出
func doStuff(ctx context.Context) {
    for {
        time.Sleep(1 * time.Second)
        select {
        case <-ctx.Done():
            logg.Printf("done")
            return
        default:
            logg.Printf("work")
        }
    }
}

func main() {
    logg = log.New(os.Stdout, "", log.Ltime)
    someHandler()
    logg.Printf("down")
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package main

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

func timeoutHandler() {
    // 创建继承Background的子节点Context
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    go doSth(ctx)

    //模拟程序运行 - Sleep 10秒
    time.Sleep(10 * time.Second)
    cancel() // 3秒后将提前取消 doSth goroutine
}

//每1秒work一下,同时会判断ctx是否被取消,如果是就退出
func doSth(ctx context.Context) {
    var i = 1
    for {
        time.Sleep(1 * time.Second)
        select {
        case <-ctx.Done():
            fmt.Println("done")
            return
        default:
            fmt.Printf("work %d seconds: \n", i)
        }
        i++
    }
}

func main() {
    fmt.Println("start...")
    timeoutHandler()
    fmt.Println("end.")
}

Context机制最核心的功能是在goroutine之间传递cancel信号,但是它的实现是不完全的。

Cancel可以细分为主动与被动两种,通过传递context参数,让调用goroutine可以主动cancel被调用goroutine。但是如何得知被调用goroutine什么时候执行完毕,这部分Context机制是没有实现的。而现实中的确又有一些这样的场景,比如一个组装数据的goroutine必须等待其他goroutine完成才可开始执行,这是context明显不够用了,必须借助 sync.WaitGroup