go Context在Go1.7引入标准库,它是Goroutine的上下文,包含其运行状态、环境、现场等信息

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{}
}
  • Deadline()
    • 返回当前Context被取消的时间,即完成工作的截止时间
  • Done()
    • 返回一个Channel,会在当前工作完成或者上下文被取消后关闭,多次调用只会返回同一个Channel
  • Err()
    • 如果当前Context被取消返回Canceled
    • 如果当前Context超时返回DeadlineExceeded
  • Value(key)
    • 从Context中返回键对应的值,对同一个上下文传入相同的key返回相同的结果
    • 仅用于传递跨API和进程间跟请求域的数据

这个接口主要被三个类继承实现,分别是emptyCtxValueCtxcancelCtx,采用匿名接口的写法,这样可以对任意实现了该接口的类型进行重写

根context

Background()TODO()是Go内置的函数,返回实现了Context接口的background和todo实例

  • Background()

    主要用于main函数、初始化以及测试代码中,作为Context树结构的最顶层,即根Context。是上下文的默认值,所有其他的上下文都应该从它衍生(Derived)出来

  • TODO()

    在不知道使用什么Context时用该函数

本质上,这两个都是emptyCtx类型,是不可取消、无截止时间、无任何值的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
type emptyCtx int
func (*emptyCtx) Deadline() (deadline time.Time, ok bool) {
return
}
func (*emptyCtx) Done() <-chan struct{} {
return nil
}
func (*emptyCtx) Err() error {
return nil
}
func (*emptyCtx) Value(key interface{}) interface{} {
return nil
}
func (e *emptyCtx) String() string {
switch e {
case background:
return "context.Background"
case todo:
return "context.TODO"
}
return "unknown empty Context"
}
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)

With函数

上面的两种方式是创建根context,不具备任何功能,具体实践还是要依靠context包提供的With系列函数来进行派生

1
2
3
4
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithValue(parent Context, key, val interface{}) Context

image-20220418205903259

WithValue

案例

e.g. 在日常开发中需要一个trace_id能够串联所有的日志,通过WithValue来创建一个携带trace_id的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
package main

import (
"context"
"fmt"
"strings"
"time"
"github.com/google/uuid"
)
const(
KEY = "trace_id"
)
func NewRequestID() string {
return strings.Replace(uuid.New().String(), "-", "", -1)
}
func NewContextWithTraceID() context.Context {
ctx := context.WithValue(context.Background(), KEY, NewRequestID())
return ctx
}
func GetContextValue(ctx context.Context, key string) string {
val, ok := ctx.Value(key).(string)
if !ok {
return ""
}
return val
}
func PrintLog(ctx context.Context, message string) {
fmt.Printf("%s|info|trace_id=%s|%s", time.Now().Format("2022-04-18 21:04:05") , GetContextValue(ctx, KEY), message)
}
func Run(ctx context.Context) {
PrintLog(ctx, "running..........")
}
func main() {
Run(NewContextWithTraceID())
}
1
2022-04-18 21:23:46.3088448 +0800 CST m=+0.002543101|info|trace_id=658abda3ae6f486c996d20fd01218bcb|running..........

通过context.Background创建了携带trace_id的ctx,其派生的任何context都会获取到该值

源码分析

WithValue重写了Value()

1
2
3
4
5
6
7
8
9
10
11
12
13
func WithValue(parent Context, key, val interface{}) Context {
if parent == nil {
panic("cannot create context from nil parent")
}
if key == nil {
panic("nil key")
}
if !reflectlite.TypeOf(key).Comparable() {
panic("key is not comparable")
}
//返回一个valueCtx结构体
return &valueCtx{parent, key, val}
}
1
2
3
4
5
//valueCtx包含父context与一个键值对
type valueCtx struct {
Context
key, val interface{}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//通过String方法输出Context与其携带的键值对
func (c *valueCtx) String() string {
return contextName(c.Context) + ".WithValue(type " +
reflectlite.TypeOf(c.key).String() +
", val " + stringify(c.val) + ")"
}
//可以通过key来获取value
func (c *valueCtx) Value(key interface{}) interface{} {
//当key匹配当前ctx时返回该value,否则去找父ctx的key,直到最后返回根ctx的nil
if c.key == key {
return c.val
}
return c.Context.Value(key)
}

image-20220418214501052

注意事项

  • 不建议使用context值传递关键参数,关键参数应显示地声明出来,不要隐式处理。context中适合携带签名、trace_id等值
  • 为避免context因多个包同时使用context冲突,key应采用内置类型
  • context传递的数据中key、value都是interface类型(编译器无法确定类型),所以应该在类型断言时保证程序的健壮性

WithTimeout, WithDeadline

这两个函数都是用来超时控制的,避免服务端长时间响应消耗资源。当一次请求达到超时时间时就会取消,不会继续传递

WithTimeout, WithDeadline两个函数的作用是一样的,区别在于传递的时间参数不同:
WithTimeout将持续时间作为参数输入而不是时间对象

案例

  • 达到超时时间终止接下来的执行操作
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
package main

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

func NewContextWithTimeout() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 3 * time.Second)
}

func HttpHandler() {
ctx, cancel := NewContextWithTimeout()
defer cancel()
deal(ctx)
}

func deal(ctx context.Context) {
for i := 0; i <10; i++ {
time.Sleep(1 * time.Second)
select {
case <- ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Printf("deal time is %d\n", i)
}
}
}

func main() {
HttpHandler()
}
1
2
3
4
//PS E:\Project\Go\src\test> go run main.go
deal time is 0
deal time is 1
context deadline exceeded
  • 没有到达超时时间终止接下来的执行
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
package main

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

func NewContextWithTimeout() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), 3 * time.Second)
}

func HttpHandler() {
ctx, cancel := NewContextWithTimeout()
defer cancel()
deal(ctx, cancel)
}

func deal(ctx context.Context, cancel context.CancelFunc) {
for i := 0; i <10; i++ {
time.Sleep(1 * time.Second)
select {
case <- ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Printf("deal time is %d\n", i)
cancel()
}
}
}

func main() {
HttpHandler()
}
1
2
3
//PS E:\Project\Go\src\test> go run main.go
deal time is 0
context canceled

withtimout返回的context会在超时时间到时自动取消,同时也可以使用返回的cancelFunc手动取消

源码分析

WithTimeout内部调用WithDeadline方法

1
2
3
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
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
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc) {
// 不能为空`context`创建衍生context
if parent == nil {
panic("cannot create context from nil parent")
}

// 当父context的结束时间早于要设置的时间,则不需要再去单独处理子节点的定时器了
if cur, ok := parent.Deadline(); ok && cur.Before(d) {
// The current deadline is already sooner than the new one.
return WithCancel(parent)
}
// 创建一个timerCtx对象
c := &timerCtx{
cancelCtx: newCancelCtx(parent),
deadline: d,
}
// 将当前节点挂到父节点上
propagateCancel(parent, c)

// 获取过期时间
dur := time.Until(d)
// 当前时间已经过期了则直接取消
if dur <= 0 {
c.cancel(true, DeadlineExceeded) // deadline has already passed
return c, func() { c.cancel(false, Canceled) }
}
c.mu.Lock()
defer c.mu.Unlock()
// 如果没被取消,则直接添加一个定时器,定时去取消
if c.err == nil {
c.timer = time.AfterFunc(dur, func() {
c.cancel(true, DeadlineExceeded)
})
}
return c, func() { c.cancel(true, Canceled) }
}

WithCancel

案例

日常开发中往往会开多个goroutine去处理事情,我们可以使用WithCancel来衍生一个context传递到不同的goroutine中,通过cancel来控制其停止运行

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
package main

import(
"context"
"time"
"fmt"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go Speak(ctx)
time.Sleep(10 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}

func Speak(ctx context.Context) {
for range time.Tick(time.Second) {
select {
case <- ctx.Done():
fmt.Println("shut up")
return
default:
fmt.Println("balahlahlahlahlah")
}
}
}
1
2
3
4
5
6
7
8
9
10
11
//PS E:\Project\Go\src\test> go run main.go
balahlahlahlahlah
balahlahlahlahlah
balahlahlahlahlah
balahlahlahlahlah
balahlahlahlahlah
balahlahlahlahlah
balahlahlahlahlah
balahlahlahlahlah
balahlahlahlahlah
shut up

源码分析

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
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
if parent == nil {
panic("cannot create context from nil parent")
}
//调用newCancelCtx(parent)返回一个cancelCtx
c := newCancelCtx(parent)
//传递cancel到child ctx
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
//=======================================================

func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
type cancelCtx struct {
Context

mu sync.Mutex // protects following fields
done chan struct{} // created lazily, closed by first cancel call
children map[canceler]struct{} // set to nil by the first cancel call
err error // set to non-nil by the first cancel call
}
type canceler interface {
cancel(removeFromParent bool, err error)
Done() <-chan struct{}
}

cancelCtx实现了Done()和Value()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (c *cancelCtx) Done() <-chan struct{} {
c.mu.Lock()
if c.done == nil {
c.done = make(chan struct{})
}
d := c.done
c.mu.Unlock()
return d

//返回cancelCtx本身或子WithValue的value
func (c *cancelCtx) Value(key interface{}) interface{} {
if key == &cancelCtxKey {
return c
}
return c.Context.Value(key)
}
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
func propagateCancel(parent Context, child canceler) {
//若返回nil,说明parent是个空结点
done := parent.Done()
if done == nil {
return // parent is never canceled
}

//判断parent ctx是否被取消
select {
//parent已经被取消,取消子ctx
case <-done:
// parent is already canceled
child.cancel(false, parent.Err())
return
default:
}
//判断父结点是否cancelCtx
if p, ok := parentCancelCtx(parent); ok {
p.mu.Lock()
if p.err != nil {
// parent has already been canceled
child.cancel(false, p.err)
} else {
if p.children == nil {
//将子结点加入父节点的childrenMap中
p.children = make(map[canceler]struct{})
}
p.children[child] = struct{}{}
}
p.mu.Unlock()
} else {
atomic.AddInt32(&goroutines, +1)
go func() {
select {
case <-parent.Done():
child.cancel(false, parent.Err())
case <-child.Done():
}
}()
}
}

cancel方法

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
func (c *cancelCtx) cancel(removeFromParent bool, err error) {
// 取消时传入的error信息不能为nil, context定义了默认error:var Canceled = errors.New("context canceled")
if err == nil {
panic("context: internal error: missing cancel error")
}
// 已经有错误信息了,说明当前节点已经被取消过了
c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}

c.err = err
// 用来关闭channel,通知其他协程
d, _ := c.done.Load().(chan struct{})
if d == nil {
c.done.Store(closedchan)
} else {
close(d)
}
// 当前节点向下取消,遍历它的所有子节点,然后取消
for child := range c.children {
// NOTE: acquiring the child's lock while holding parent's lock.
child.cancel(false, err)
}
// 节点置空
c.children = nil
c.mu.Unlock()
// 把当前节点从父节点中移除,只有在外部父节点调用时才会传true
// 其他都是传false,内部调用都会因为c.children = nil被剔除出去
if removeFromParent {
removeChild(c.Context, c)
}
}