Context 是什么?

  • Context 是 Go 中用来传递上下文信息的一种方式,要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等
  • 常见场景:当作为 server 处理 client 信息时,client 异常关闭之后,server 应该同步终止 client 之前的请求资源,即 gracefully terminate(这样server能够节省资源,不处理 client 多余的请求,毕竟 client 已经异常关闭)

Context 如何使用?

基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"context"
"fmt"
)

func doSomething(ctx context.Context) {
fmt.Println("Doing something!")
}

func main() {
ctx := context.TODO()
doSomething(ctx)
}

传递变量

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

import (
"context"
"fmt"
)

func doSomething(ctx context.Context) {
fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey"))

anotherCtx := context.WithValue(ctx, "myKey", "anotherValue")
doAnother(anotherCtx)

fmt.Printf("doSomething: myKey's value is %s\n", ctx.Value("myKey"))
}

func doAnother(ctx context.Context) {
fmt.Printf("doAnother: myKey's value is %s\n", ctx.Value("myKey"))
}

func main() {
ctx := context.Background()

ctx = context.WithValue(ctx, "myKey", "myValue")

doSomething(ctx)
}

// Output:
// doSomething: myKey's value is myValue
// doAnother: myKey's value is anotherValue
// doSomething: myKey's value is myValue
  • 通过 context可以用来传递参数,此时可在调用链中继续使用,注意使用场景,简而言之,就是不要滥用,下面给出了一个注意点

    Contexts can be a powerful tool with all the values they can hold, but a balance needs to be struck between data being stored in a context and data being passed to a function as parameters. It may seem tempting to put all of your data in a context and use that data in your functions instead of parameters, but that can lead to code that is hard to read and maintain. A good rule of thumb is that any data required for a function to run should be passed as parameters. Sometimes, for example, it can be useful to keep values such as usernames in context values for use when logging information for later. However, if the username is used to determine if a function should display some specific information, you’d want to include it as a function parameter even if it’s already available from the context. This way when you, or someone else, looks at the function in the future, it’s easier to see which data is actually being used.

  • context 传递变量时,同一个函数中,值不会被改变

通过 语义 终止运行/同步信号(context.WithCancel)

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
42
43
44
45
46
47
package main

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

func doSomething(ctx context.Context) {
ctx, cancelCtx := context.WithCancel(ctx)

printCh := make(chan int)
go doAnother(ctx, printCh)

for num := 1; num <= 3; num++ {
printCh <- num
}

cancelCtx()

time.Sleep(100 * time.Millisecond)

fmt.Printf("doSomething: finished\n")
}

func doAnother(ctx context.Context, printCh <-chan int) {
for {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
fmt.Printf("doAnother err: %s\n", err)
}
fmt.Printf("doAnother: finished\n")
return
case num := <-printCh:
fmt.Printf("doAnother: %d\n", num)
}
}
}

// Output
// doAnother: 1
// doAnother: 2
// doAnother: 3
// doAnother err: context canceled
// doAnother: finished
// doSomething: finished
  • 通过向 channel 传递消息,来决定是否终止后续运行。逐步传递,保证上层 Goroutine 执行出现错误时,将信号及时同步给下层
  • ctx.Done()被执行,说明已到达设定的终止语义

通过 终止时间 终止运行

context.WithDeadline

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 doSomething(ctx context.Context) {
deadline := time.Now().Add(1500 * time.Millisecond)
ctx, cancelCtx := context.WithDeadline(ctx, deadline)
defer cancelCtx()

printCh := make(chan int)
go doAnother(ctx, printCh)

for num := 1; num <= 3; num++ {
select {
case printCh <- num:
time.Sleep(1 * time.Second)
case <-ctx.Done():
break
}
}

cancelCtx()

time.Sleep(100 * time.Millisecond)

fmt.Printf("doSomething: finished\n")
}

...
  • 通过指定超时时间,来决定是否终止后续运行。如果超时,则会自动终止调用

  • 同时使用 defer cancelCtx()cancelCtx()原因: 后者是主动调用,没有回收一些资源(因为写的是 break),前者是更加安全的方式

    The defer cancelCtx() isn’t necessarily required because the other call will always be run, but it can be useful to keep it in case there are any return statements in the future that cause it to be missed. When a context is canceled from a deadline, the cancel function is still required to be called in order to clean up any resources that were used, so this is more of a safety measure.

context.WithTimeout

1
2
3
4
5
6
7
8
9
10
...

func doSomething(ctx context.Context) {
ctx, cancelCtx := context.WithTimeout(ctx, 1500*time.Millisecond)
defer cancelCtx()

...
}

...
  • 简化使用

参考