golang 的 context
在Go语言中,context
(上下文)是一个标准库包context
中定义的类型,用于在不同的 Goroutine(Go协程)之间传递请求范围的数据、取消信号和截止时间等信息。
context.Context
类型是一个接口,它定义了一组方法,用于管理和传递请求范围的上下文数据。这些方法包括:
context.WithCancel(parent Context) (ctx Context, cancelFunc CancelFunc)
:创建一个带有取消信号的子上下文。当父上下文被取消时,子上下文也会被取消。context.WithDeadline(parent Context, deadline time.Time) (Context, CancelFunc)
:创建一个带有截止时间的子上下文。当截止时间到达时,子上下文会被取消。context.WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
:创建一个带有超时时间的子上下文。超过指定的时间后,子上下文会被取消。context.WithValue(parent Context, key interface{}, value interface{}) Context
:创建一个带有键值对的子上下文。可以通过这个方法在上下文中传递请求范围的数据。
上下文对象可以通过函数调用链传递给需要访问请求范围数据的各个函数或方法。通过在上下文中传递取消信号,可以在某些情况下优雅地取消正在进行的操作,如停止长时间运行的任务或终止正在进行的网络请求。
使用上下文的一个常见场景是在并发处理中。例如,在处理HTTP请求时,可以在处理程序函数中创建一个上下文对象,并将其传递给执行具体业务逻辑的 Goroutine。在需要取消请求时,可以调用上下文的取消方法,通知相关的 Goroutine 停止处理,并释放相关资源。
下面是一个简单的示例,展示了如何使用上下文进行请求取消:
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
http.HandleFunc("/hello", handleRequest)
http.ListenAndServe(":8080", nil)
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
// 创建一个父上下文
parentCtx := r.Context()
// 创建带有取消信号的子上下文,设置取消时间为2秒后
ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()
// 模拟一个耗时操作
go performTask(ctx)
select {
case <-ctx.Done():
// 上下文被取消,请求超时
fmt.Println("请求超时")
w.WriteHeader(http.StatusRequestTimeout)
case <-time.After(1 * time.Second):
// 假设任务在1秒内完成
fmt.Println("请求成功")
w.Write([]byte("Hello, World!"))
}
}
func performTask(ctx context.Context) {
// 模拟一个耗时操作,需要2秒
time.Sleep(2 * time.Second)
// 检查上下文是否被取消
if ctx.Err() != nil {
fmt.Println("任务被取消")
return
}
// 任务完成
fmt.Println("任务完成")
}
在上述示例中,我们创建了一个HTTP服务器,处理/hello
路径的请求。在处理函数中,我们创建了一个父上下文parentCtx
,然后使用context.WithTimeout
函数创建了一个带有2秒超时时间的子上下文ctx
。然后,我们启动一个 Goroutine 来执行模拟的耗时任务performTask
。在主函数中的select
语句中,我们使用ctx.Done()
通道来检测上下文是否被取消。如果超时时间到达,ctx.Done()
会被关闭,从而触发相应的处理操作。
总结:context
是Go语言中用于在不同 Goroutine 之间传递请求范围数据、取消信号和截止时间等信息的机制。通过使用上下文,可以优雅地取消请求、管理超时和传递数据。上下文对象可以通过函数调用链传递,并在需要时取消相关操作。
Go 上下文 Context
Go 语言中用来设置截止日期、同步信号,传递请求相关值的结构体。
Go 语言 1.7 版本中引入 context.Context 标准库的接口,该接口定义了四个需要实现的方法:
Deadline
返回 context.Context 被取消的时间,也就是完成工作的截止日期;
Done
返回一个 Channel,这个 Channel 会在当前工作完成或者上下文被取消后关闭,多次调用 Done 方法会返回同一个 Channel;
Err
返回 context.Context 结束的原因,它只会在 Done 方法对应的 Channel 关闭时返回非空的值;
1 如果 context.Context 被取消,会返回 Canceled 错误;
2 如果 context.Context 超时,会返回 DeadlineExceeded 错误;
Value
从 context.Context 中获取键对应的值,对于同一个上下文来说,多次调用 Value 并传入相同的 Key 会返回相同的结果,该方法可以用来传递请求特定的数据;
context 包接口
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
context 包中提供:
context.Background、
context.TODO、
context.WithDeadline、
context.WithValue
函数会返回实现该接口的私有结构体,我们会在后面详细介绍它们的工作原理。
设计原理
在 Goroutine 构成的树形结构中对信号进行同步以减少计算资源的浪费是 context.Context 的最大作用。
Go 服务的每一个请求都是通过单独的 Goroutine 处理的,HTTP/RPC 请求的处理器会启动新的 Goroutine 访问数据库和其他服务。
我们可能会创建多个 Goroutine 来处理一次请求,而 context.Context 的作用是在不同 Goroutine 之间同步请求特定数据、取消信号以及处理请求的截止日期。
Context 与 Goroutine 树
每一个 context.Context 都会从最顶层的 Goroutine 一层一层传递到最下层。
context.Context 可以在上层 Goroutine 执行出现错误时,将信号及时同步给下层。
不使用 Context 同步信号
当最上层的 Goroutine 因为某些原因执行失败时,下层的 Goroutine 由于没有接收到这个信号所以会继续工作;
但是当我们正确地使用 context.Context 时,就可以在下层及时停掉无用的工作以减少额外资源的消耗:
使用 Context 同步信号
可以通过一个代码片段了解 context.Context 是如何对信号进行同步的。
在这段代码中,我们创建了一个过期时间为 1s 的上下文,并向上下文传入 handle 函数,该方法会使用 500ms 的时间处理传入的请求:
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
go handle(ctx, 500*time.Millisecond)
select {
case <-ctx.Done():
fmt.Println("main", ctx.Err())
}
}
func handle(ctx context.Context, duration time.Duration) {
select {
case <-ctx.Done():
fmt.Println("handle", ctx.Err())
case <-time.After(duration):
fmt.Println("process request with", duration)
}
}
因为过期时间大于处理时间,
所以我们有足够的时间处理该请求,
运行上述代码会打印出下面的内容:
$ go run context.go
process request with 500ms
main context deadline exceeded
handle 函数没有进入超时的 select 分支,但是 main 函数的 select 却会等待 context.Context 超时并打印出 main context deadline exceeded。
如果我们将处理请求时间增加至 1500ms,整个程序都会因为上下文的过期而被中止:
go handle(ctx, 1500*time.Millisecond)
$ go run context.go
main context deadline exceeded
handle context deadline exceeded
相信这两个例子能够帮助各位读者理解 context.Context 的使用方法和设计原理 — 多个 Goroutine 同时订阅 ctx.Done() 管道中的消息,一旦接收到取消信号就立刻停止当前正在执行的工作。
默认上下文
context 包中最常用的方法还是 context.Background、context.TODO,
这两个方法都会返回预先初始化好的私有变量 background 和 todo,它们会在同一个 Go 程序中被复用:
func Background() Context {
return background
}
func TODO() Context {
return todo
}
这两个私有变量都是通过 new(emptyCtx) 语句初始化的,
它们是指向私有结构体 context.emptyCtx 的指针,
这是最简单、最常用的上下文类型:
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
}
从上述代码中,我们不难发现 context.emptyCtx 通过空方法实现了 context.Context 接口中的所有方法,它没有任何功能。
Context 层级关系
从源代码来看,context.Background 和 context.TODO 也只是互为别名,没有太大的差别,只是在使用和语义上稍有不同:
- context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生出来;
- context.TODO 应该仅在不确定应该使用哪种上下文时使用;
在多数情况下,如果当前函数没有上下文作为入参,我们都会使用 context.Background 作为起始的上下文向下传递。
示例
context.WithValue 根据上下文添加和读取价值
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
ctx = addValue(ctx)
readValue(ctx)
}
func addValue(ctx context.Context) context.Context {
return context.WithValue(ctx, "key", "test-value")
}
func readValue(ctx context.Context) {
val := ctx.Value("key")
fmt.Println(val)
}
test-value
HTTP 中间件将 guid 添加到请求上下文的中间件
package main
import (
"context"
"log"
"net/http"
"github.com/google/uuid"
"github.com/gorilla/mux"
)
func main() {
// 创建一个新的Gorilla Mux路由器
router := mux.NewRouter()
// 使用guidMiddleware中间件,该中间件会为每个请求生成一个唯一的UUID并将其存储在请求上下文中
router.Use(guidMiddleware)
// 定义一个路由处理函数,处理GET请求到"/ishealthy"路径
router.HandleFunc("/ishealthy", handleIsHealthy).Methods(http.MethodGet)
// 监听端口8080,并将请求交给Mux路由器处理
http.ListenAndServe(":8080", router)
}
// handleIsHealthy是处理"/ishealthy"路径的路由处理函数
func handleIsHealthy(w http.ResponseWriter, r *http.Request) {
// 设置HTTP响应状态码为200 - 健康状态
w.WriteHeader(http.StatusOK)
// 从请求上下文中获取之前生成的UUID
uuid := r.Context().Value("uuid")
// 在日志中记录响应以及相关UUID
log.Printf("[%v] Returning 200 - Healthy", uuid)
// 向客户端发送"Healthy"作为响应主体
w.Write([]byte("Healthy"))
}
// guidMiddleware是一个中间件函数,用于生成唯一的UUID并将其存储在请求上下文中
func guidMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 生成一个新的UUID
uuid := uuid.New()
// 将UUID存储在请求上下文中,以便后续处理函数可以访问它
r = r.WithContext(context.WithValue(r.Context(), "uuid", uuid))
// 调用下一个处理程序(通常是路由处理函数)
next.ServeHTTP(w, r)
})
}
context.Background()、context.TODO() 生成和使用两者
package main
import (
"context"
"fmt"
)
func thingThatTakesContext(ctx context.Context) {
fmt.Println("hello, world")
}
func main() {
ctxBackground := context.Background()
ctxTODO := context.TODO()
thingThatTakesContext(ctxBackground)
thingThatTakesContext(ctxTODO)
}
hello, world
hello, world
WithCancel 返回可取消的上下文
生成一个可取消的上下文,并通检查完成通道来检查它是否已被取消Done()
package main
import (
"context"
"fmt"
"time"
)
func longRunningTask(ctx context.Context) {
fmt.Println("后台长时间运行任务启动")
// select {
// case <-ctx.Done():
// fmt.Println("由于上下文取消,长时间运行的任务被保释")
// }
// 更简洁的办法
<-ctx.Done()
fmt.Println("由于上下文取消,长时间运行的任务被保释")
}
func main() {
/*
当cancelFunc被调用时,这个函数将会退出
WithCancel返回一个带有Done通道的parent副本。
当返回的cancel函数被调用或父上下文的Done通道被关闭时,
以先发生的为准关闭返回上下文的Done通道。
*/
ctx, cancelFunc := context.WithCancel(context.Background())
go longRunningTask(ctx)
time.Sleep(time.Second)
fmt.Println("后台长时间运行的任务仍在运行")
time.Sleep(time.Second)
fmt.Println("要取消后台任务")
cancelFunc()
time.Sleep(time.Second)
fmt.Println("取消后已经过了一段时间")
}
WithDeadline 返回带有截止日期的上下文
WithDeadline 返回一个带有截止日期调整的父上下文的副本
package main
import (
"context"
"fmt"
"time"
)
func longRunningTask(ctx context.Context, timeToRun time.Duration) {
select {
case <-time.After(timeToRun):
fmt.Println("在上下文截止日期之前完成")
case <-ctx.Done():
fmt.Println("因为上下文截止日期已过,放弃了")
}
}
const duration = 5 * time.Second
func main() {
ctx := context.Background()
// 这将放弃,因为函数的运行时间超过了上下文的截止日期允许的时间。
ctx1, _ := context.WithDeadline(ctx, time.Now().Add(duration))
/*
WithDeadline 返回一个带有截止日期调整的父上下文的副本,
截止日期不会晚于 `d`。如果父上下文的截止日期已经早于 `d`,
那么 `WithDeadline(parent, d)` 在语义上等同于 `parent`。
当截止日期到期、调用返回的取消函数或父上下文的 Done 通道关闭时,
返回的上下文的 Done 通道也会关闭,以先发生的事件为准。
取消此上下文会释放与其关联的资源,
因此代码应该在运行在此上下文中的操作完成后尽快调用 cancel。
*/
longRunningTask(ctx1, 10*time.Second)
// 这将完成,因为函数在上下文的截止日期到来之前完成。
ctx2, _ := context.WithDeadline(ctx, time.Now().Add(duration))
longRunningTask(ctx2, 3*time.Second)
}
因为上下文截止日期已过,放弃了
在上下文截止日期之前完成
WithTimeout 超时上下文
package main
import (
"context"
"fmt"
"time"
)
func longRunningTask(ctx context.Context, timeToRun time.Duration) {
select {
case <-time.After(timeToRun):
fmt.Println("在上下文超时之前完成")
case <-ctx.Done():
fmt.Println("因为上下文超时而退出")
}
}
const timeout = 5 * time.Second
func main() {
ctx := context.Background()
// 这将会因为函数运行时间超过上下文允许的时间而退出。
ctx1, _ := context.WithTimeout(ctx, timeout)
longRunningTask(ctx1, 10*time.Second)
// 这将会完成,因为函数在上下文超时之前完成。
ctx2, _ := context.WithTimeout(ctx, timeout)
longRunningTask(ctx2, 1*time.Second)
}
context.WithValue 具有值的上下文
package main
import (
"context"
"fmt"
)
func tryAnotherKeyType(ctx context.Context, keyToConvert string) {
type keyType2 string
k := keyType2(keyToConvert)
if v := ctx.Value(k); v != nil {
fmt.Println("找到键类型2的值:", v)
} else {
fmt.Println("没有键类型值 2")
}
}
func main() {
keyString := "foo"
type keyType1 string
k := keyType1(keyString)
ctx := context.WithValue(context.Background(), k, "bar")
if v := ctx.Value(k); v != nil {
fmt.Println("找到键类型的值 1:", v)
} else {
fmt.Println("没有键类型值 1")
}
tryAnotherKeyType(ctx, keyString)
}
找到键类型的值 1: bar
没有键类型值 2