目录:
1、什么是context
2、context用法
- 2.1、创建子context
- 2.2、创建context 树
- 2.3、如何使用
- 2.4、使用注意事项
1、什么是context:
Go 1.7 标准库引入 context,中文译作“上下文”,准确说它是 goroutine(协程) 的上下文,包含 goroutine 的运行状态、环境、现场等信息。context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。
context.Context 类型的值可以协调多个 groutine 中的代码执行“取消”操作,并且可以存储键值对。最重要的是它是并发安全的。
与它协作的 API 都可以由外部控制执行“取消”操作,例如:取消一个 HTTP 请求的执行。
在Go 里,我们不能直接杀死协程,协程的关闭一般会用 channel+select 方式来控制。但是在某些场景下,例如处理一个请求衍生了很多协程,这些协程之间是相互关联的:需要共享一些全局变量、有共同的 deadline 等,而且可以同时被关闭。再用 channel+select 就会比较麻烦,这时就可以通过 context 来实现。
单的说context 用来解决 goroutine 之间退出通知、 元数据传递的功能。
2、context用法:
假设你开启了一个函数,你需要将一些常用的值传递给下游函数,但是不能通过函数参数传递,怎么办?
假设你开启了一个协程 A,协程 A 衍生出很多子协程,这些子协程又衍生出子协程,如果协程 A 所完成的任务“成果”不再需要,那我们怎么通知衍生出的子协程及时退出并释放占用的系统资源呢?
假设一个任务需要在 2s 内完成,如果超时,如何优雅地退出返回呢?
假设一个任务需要在中午 12 点完成,如果到点没有完成,又该如何优雅地退出呢?
golang提供了context包来处理上面的问题:
理解 context 包,核心是需要理解 Context 接口:
type Context interface {
Done() <-chan struct{}
Err() error
Deadline() (deadline time.Time, ok bool)
Value(key interface{}) interface{}
}
这个接口有四个方法:
- Done():返回只读的 channel,在 goroutine 中,如果该 channel 可读,则意味着父 context 发起了取消操作或者是时间到期,理解这一点非常重要。
- Err():返回错误,表示 channel 被关闭的原因,被取消还是超时。
- Deadline():获取设置的截止时间,两个返回的参数第一个是截止时间,表示到了这个点,context 会自动发起取消操作;第二个表示是否设置了截止时间。
- Value():方法获取 context 上绑定的值,是一个键值对,这个值一般是线程安全的。
2.1、创建子context:
我们不需要自己实现 Context 接口,源码包已经为我们提供了两个实现接口的方法,分别是 Background() 和 TODO();
var (
background = new(emptyCtx)
todo = new(emptyCtx)
)
func Background() Context {
return background
}
func TODO() Context {
return todo
}
context.Background() 返回空的 context,通常用在 main 函数里,作为根 context 衍生出子 context。
context.TODO() 也是返回空 context。主要用在还不清楚使用什么类型的 context 的时候,便于后期重构,先用它占个位。
它们两本质上是 emptyCtx 类型,不能被取消,没有值,也没有超时时间。
有了上面两个根 context,就可以衍生出子 context,源码包为我们提供了一系列 withXXX 函数用于生成子 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, val interface{}) Context
这四个函数的第一个参数都是父 context,可以理解为基于父 context 生成子 context,即衍生子 context。
- WithCancel() 基于父 context,返回子 context 和取消函数;
- WithDeadline() 基于父 context,返回带截止时间(截止时间可以基于当前时间延后多少时间)的子 context 和取消函数;
- WithTimeout() 基于父 context,返回带超时时间的子 context 和取消函数;
- WithValue() 基于父 context,返回绑定键值对的子 context,没有取消函数;
前三个函数都会返回取消函数,需要注意的是只有创建该 context 的协程才能调用取消函数,且不推荐将取消函数作为参数传递。
我们可以调用取消函数取消一个 context,以及这个 context 下面所有的子 context。
通过这些函数,就能生成一棵 context 树,树的每个节点都可以有任意多个子节点,节点层级可以有任意多个。
2.2、创建context 树:
我们一起来看下基于两个根 context 可以创建的 context 树是什么样。
1)、两层树:
rootCtx := context.Background()
childCtx := context.WithValue(rootCtx, "request_Id", "seekload")
上面的代码基于根 context 创建了两层 context 树,rootCtx 衍生出 childCtx,并携带键值对 {"request_Id" : "seekload"}。
2)、三层树:
rootCtx := context.Background()
childCtx := context.WithValue(rootCtx, "request_Id", "seekload")
childOfChildCtx, cancelFunc := context.WithCancel(childCtx)
基于两层树,childCtx 衍生出 childOfChildCtx,含有键值对并且具有取消功能。
3)、多层树:
rootCtx := context.Background()
childCtx1 := context.WithValue(rootCtx, "request_Id", "seekload")
childCtx2, cancelFunc := context.WithCancel(childCtx1)
childCtx3 := context.WithValue(rootCtx, "user_Id", "user_100")
上面的代码:
rootCtx 是根 context;
rootCtx 衍生出 childCtx1,并携带键值对 {"request_Id" : "seekload"};
childCtx1 衍生出 childCtx2,可以取消 context;
rootCtx 衍生出 childCtx3,携带键值对 {"user_Id" : "user_100"};
层级关系就像下面这样:
2.3、如何使用:
1)、context.WithCancel()用法:
context.WithCancel() 用于取消信号,直接来看例子:
func main() {
ctx := context.Background() //创建根context
cancelCtx, cancelFunc := context.WithCancel(ctx) //基于根创建第二层context对象,传入协程中
go task(cancelCtx)
time.Sleep(time.Second * 3)
cancelFunc() // 可以在父级对创建的子context发起取消 context(即结束协程)
time.Sleep(time.Second * 1) // 延时等待协程退出
fmt.Println("number of goroutine: ",runtime.NumGoroutine()) // 协程数量
}
func task(ctx context.Context) {
i := 1
for {
select {
case <-ctx.Done(): // 接收取消信号
fmt.Println("Gracefully exit")
fmt.Println(ctx.Err()) // 取消原因
return
default:
fmt.Println(i)
time.Sleep(time.Second * 1)
i++
}
}
}
输出:
1
2
3
Gracefully exit
context canceled
number of goroutine: 1
当调用 cancelFunc(),Done() 返回的 channel 变成可读,Err() 返回取消原因 “context canceled”,task() 函数执行 return 优雅地退出。
2)、context.WithValue()用法:
通过 context.WithValue() 可以在 goroutine 之间传递一些数据。
func main() {
helloWorldHandler := http.HandlerFunc(HelloWorld)
http.Handle("/hello", inejctRequestId(helloWorldHandler))
http.ListenAndServe(":8080", nil)
}
func HelloWorld(w http.ResponseWriter, r *http.Request) {
requestId := ""
if m := r.Context().Value("requestId"); m != nil {
if value, ok := m.(string); ok {
requestId = value
}
}
w.Header().Add("requestId", requestId)
w.Write([]byte("Hello, world"))
}
func inejctRequestId(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requestId := uuid.New().String()
ctx := context.WithValue(r.Context(), "requestId", requestId)
req := r.WithContext(ctx)
next.ServeHTTP(w, req)
})
}
上面的代码,inejctRequestId() 是请求中间函数,通过 context.WithValue() 注入了键值对;HelloWorld() 是请求处理函数,从 context 获取到刚才绑定的 k-v。
go run 上面示例,然后执行:
$curl -v localhost:8080/hello
会输出:
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /hello HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Requestid: e0b0544d-7993-4ff5-a2de-b29eacd3645a
< Date: Tue, 11 Jul 2023 08:03:26 GMT
< Content-Length: 12
< Content-Type: text/plain; charset=utf-8
<
* Connection #0 to host localhost left intact
Hello, world
从输出可以看到,返回里有 Requestid 信息。
3)、context.WithTimeout()用法:
context.WithTimeout() 可以设置一个超时时间,过期之后 channel done 会自动关闭,context 会被取消;超时之前可以调用取消函数手动取消 context。
func main() {
ctx := context.Background()
cancelCtx, cancel := context.WithTimeout(ctx, time.Second*3)
defer cancel()
go task(cancelCtx)
time.Sleep(time.Second * 4)
}
func task(ctx context.Context) {
i := 1
for {
select {
case <-ctx.Done():
fmt.Println("Gracefully exit")
fmt.Println(ctx.Err())
return
default:
fmt.Println(i)
time.Sleep(time.Second * 1)
i++
}
}
}
输出:
1
2
3
Gracefully exit
context deadline exceeded
上面的代码,context.WithTimeout() 设置了 3s 的超时时间,时间到了之后,context 自动取消,done channel 变成可读,Err() 返回取消原因,执行 return,task() 优雅地退出。
4)、context.WithDeadline()用法:
context.WithDeadline() 设置一个将来的时间点作为截止时间,时间到了之后,channel done 会自动关闭,context 会被取消;还未到截止时间可以调用取消函数手动取消 context。
func main() {
ctx := context.Background()
cancelCtx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second*3))
defer cancel()
go task(cancelCtx)
time.Sleep(time.Second * 4) // 延时,等待 task() 正常退出
}
func task(ctx context.Context) {
i := 1
for {
select {
case <-ctx.Done():
fmt.Println("Gracefully exit")
fmt.Println(ctx.Err())
return
default:
fmt.Println(i)
time.Sleep(time.Second * 1)
i++
}
}
}
上面的代码设置的截止时间是 3s 钟之后的时间点,时间到了之后,context 自动取消,done channel 变成可读,Err() 返回取消原因,执行 return,task() 优雅地退出。
输出:
1
2
3
Gracefully exit
context deadline exceeded
2.4、使用注意事项:
- 永远不要传递不存在的 context 。相反,如果您不确定使用什么,使用一个 ToDo context。
- context.Background 只应用在最高等级,作为所有派生 context 的根。
- Context 结构没有取消方法,因为只有派生 context 的函数才应该取消 context。