Context简介:
Context由Google官方开发,在1.7版本引入, 在Go服务器程序中,每个请求都会有一个goroutine去处理。然而,处理程序往往还需要创建额外的goroutine去访问后端资源,比如数据库、RPC服务等。由于这些goroutine都是在处理同一个请求,所以它们往往需要访问一些共享的资源,比如用户身份信息、认证token、请求截止时间等。而且如果请求超时或者被取消后,所有的goroutine都应该马上退出并且释放相关的资源。Go提供了一个Context包,也就是上下文,来控制它们达到目的,本文我们来介绍Context包的基本使用方法。
Context使用:
在请求处理的过程中,会调用各层的函数,每层的函数会创建自己的routine,构成了一个routine树, 如果由Context来控制上下文,context也应该反映并实现成一棵树。
根节点
要创建context树,第一步是要有一个根结点。context.Background
函数的返回值是一个空的context,经常作为树的根结点,它一般由接收请求的第一个routine创建,不能被取消、没有值、也没有过期时间
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
// Background returns a non-nil, empty Context. It is never canceled, has no
// values, and has no deadline. It is typically used by the main function,
// initialization, and tests, and as the top-level Context for incoming
// requests.
func Background() Context {
return background
}
type emptyCtx int
其中emptyCtx 实现了Context接口。
子节点
context包为我们提供了以下函数:
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 interface{}, val interface{}) Context
这四个函数的第一个参数都是父context,返回一个Context类型的值,这样就层层创建出不同的节点。子节点是从复制父节点得到的,并且根据接收的函数参数保存子节点的一些状态值,然后就可以将它传递给下层的routine了。
package main
import (
"context"
"fmt"
"time"
)
func Println(ctx context.Context, a, b int) {
for {
fmt.Println(a + b)
a, b = a+1, b+1
select {
case <-ctx.Done():
fmt.Println("程序结束")
return
default:
}
}
}
func main() {
{
// 超时取消
a := 1
b := 2
timeout := 2 * time.Second
ctxBg := context.Background()
ctx, _ := context.WithTimeout(ctxBg, timeout)
Println(ctx, a, b)
time.Sleep(2 * time.Second) // 等待时候还会继续输出
}
{
// 手动取消
a := 1
b := 2
ctx, cancelCtx := context.WithCancel(context.Background())
go func() {
time.Sleep(2 * time.Second)
cancelCtx() // 在调用处主动取消
}()
Println(ctx, a, b)
time.Sleep(2 * time.Second)
}
}
有一个大胆的想法,如果改动手动取消代码如下:
{
// 手动取消
a := 1
b := 2
ctx, _ := context.WithCancel(context.Background())
go func() {
time.Sleep(2 * time.Second)
// cancelCtx() // 注释了主动的取消代码
}()
Println(ctx, a, b)
time.Sleep(2 * time.Second)
}
程序会有怎样进行呢?动手试试吧。
再来看看WithTimeout的源码吧
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
return WithDeadline(parent, time.Now().Add(timeout))
}
要注意的WithDeadline的第二个参数和WithCancel的区别。
验证超时
func doTimeOutStuff(ctx context.Context) {
for {
time.Sleep(1 * time.Second)
if deadline, ok := ctx.Deadline(); ok {
fmt.Println("deadline set : ", deadline.Unix())
if time.Now().After(deadline) {
log.Fatal("错误:", ctx.Err().Error())
}
}
select {
case <-ctx.Done():
fmt.Println("Done")
return
default:
fmt.Println("working")
}
}
}
func main() {
ctx, _ := context.WithTimeout(context.Background(), 5*time.Second)
fmt.Println("当前时间: ", time.Now().Unix())
fmt.Println("超时时间: 5秒")
go doTimeOutStuff(ctx)
time.Sleep(10 * time.Second)
}
验证是否超时做日志记录处理,或者根据业务做出相应的提示。 在<-ctx.Done() 做好资源的控制,必满资源浪费。
返回值CancelFunc
调用CancelFunc, 会取消child以及child生成的context,取出父context对这个child的引用,停止相关的计数器。
WithValue
package main
import (
"context"
"fmt"
"time"
)
var key string = "name"
func main() {
ctx, cancel := context.WithCancel(context.Background())
//附加值
valueCtx := context.WithValue(ctx, key, "【监控1】")
go watch(valueCtx)
time.Sleep(10 * time.Second)
fmt.Println("可以了,通知监控停止")
cancel()
//为了检测监控过是否停止,如果没有监控输出,就表示停止了
time.Sleep(5 * time.Second)
}
func watch(ctx context.Context) {
for {
select {
case <-ctx.Done():
//取出值
fmt.Println(ctx.Value(key), "监控退出,停止了...")
return
default:
//取出值
fmt.Println(ctx.Value(key), "goroutine监控中...")
time.Sleep(2 * time.Second)
}
}
}
Context 使用原则
- 不要把Context放在结构体中,要以参数的方式传递
- 以Context作为参数的函数方法,应该把Context作为第一个参数,放在第一位。
- 给一个函数方法传递Context的时候,不要传递nil,如果不知道传递什么,就使用context.TODO
- Context的Value相关方法应该传递必须的数据,不要什么数据都使用这个传递
- Context是线程安全的,可以放心的在多个goroutine中传递