如果你是 Golang 新手,你有可能几乎在任何地方都见过这个模块,但有时会发现它令人困惑,甚至是抽象的。
事实是,这个模块无处不在是有原因的。它在维护你的应用程序性能方面起着至关重要的作用。
在这篇博文中,我将为你省去翻阅源代码的麻烦,并告诉你关于该模块的一切!context
让我们开始吧!
一、为什么我们需要 Context ?
想象一下,你是一家餐厅的接单员。
当一个订单到达时,你把它委托给众多厨师中的一个。
如果顾客突然决定走开,你会怎么做?
毫无疑问,你会阻止你的厨师进一步处理这个订单,以防止任何原料的浪费!
这就是这个 context 的作用!
它是传递给你的函数和 Goroutines 的一个参数,并允许你在不再需要它们时及时停止它们。
二、什么是 Context?
该模块的典型用法是当客户端终止与服务器的连接时。
如果终止发生在服务器正在进行一些繁重的工作或数据库查询时呢?
该模块允许这些处理在不再需要的情况下立即停止。
Context 使用可归结为三个主要部分:
- 侦听取消事件
- 发出一个取消事件
- 传递请求范围数据
让我们分别讨论这些内容。
三、监听取消事件
type Context interface {
Done() <- chan struct{}
Err() error
Deadline() (deadline time.Time, ok bool)
Value(key interface{}) interface{}
}
Context 类型只不过是一个接口,实现了四个简单的函数。
现在,我们先关注前两个:Done() 和 Err()
当 context 被 cancel 时,Done() 函数返回一个接收空 struct 的 channel。
Err() 函数在 cancel 的情况下返回一个非 nil 的 error,否则返回 nil。
使用这些函数,监听取消事件变得很容易。
1. 监听 Done() 通道
go
func main() {
http.ListenAndServe(":8000", http.HandlerFunc(handler))
}
func handler(
w http.ResponseWriter,
r *http.Request,
) {
ctx := r.Context()
select {
case <-time.After(2 * time.Second):
w.Write([]byte("request processed"))
case <- ctx.Done():
fmt.Println("request cancelled")
return
}
}
在上面的例子中,我们模拟了一个网络服务处理器。
我们用 time.After() 来模拟了一个需要两秒钟处理一个请求的函数。
如果 context 在两秒内被取消,ctx.Done() 会收到一个空的 struct 。第二种情况将被执行,并且退出函数。
你可以在你的本地启动这段代码。一旦启动,在你的浏览器上访问 localhost:8000,然后在两秒内关闭它。观察你的终端,看看会发生什么。
2. 检查 Err() 的错误
func handler(ctx context.Context) {
if ctx.Err() != nil {
fmt.Println("Context is cancelled")
return
}
fmt.Println("Processing request")
}
另外,你可以在执行一些关键逻辑之前从 ctx.Err() 检查错误。
如果上下文被取消了,上面的函数就会停止并返回。
四、发出一个取消事件
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
context 提供了三个返回 CancelFunc 的函数。
调用 cancelFunc 会向 ctx.Done() 通道发射一个空的 struct,并通知监听它的下游函数。
在深入研究它们之前,我们先来谈谈上下文树(context tree)和根上下文(root context)。
1. Context Tree
当你调用这些 WithX 函数时,它们接受一个父级 context,并返回一个带有新的 Done 通道的 context 副本。
less
rootCtx := context.Background()
child1Ctx, cancelFunc1 := context.WithCancel(rootCtx)
child2Ctx, cancelFunc2 := context.WithCancel(rootCtx)
child3Ctx, cancelFunc3 := context.WithCancel(child1Ctx)
在上面的例子中,我们创建了一个多上下文树(context tree)。
当我们调用 cancelFunc1,我们将取消 child1Ctx 和 child3Ctx,child2Ctx不受影响。
2. Root context
由于这些函数需要一个父 context 作为参数,context 提供了两个简单的函数来创建 root context。
func Background() Context
func TODO() Context
这些函数输出一个空的 context,完全不做任何事情。它不能被取消,也不能携带值。
它们的主要目的是作为一个根 context,以后可以传递给任何一个 WithX 函数来创建一个可取消的 context。
3. WithCancel
go
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
cancel()
// Output: context cancelled
fmt.Println(ctx.Err().Error())
WithCancel 函数接收一个父 context,并返回一个可取消的 contet 和一个取消函数。
go
func handler() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
go operation1(ctx)
data, err := databaseQuery()
if err != nil {
cancel()
}
}
func operation1(ctx context.Context) {
for {
select {
case <- ctx.Done():
fmt.Println(ctx.Err().Error())
return
default:
fmt.Println("Do something")
}
}
}
如果 databaseQuery 返回一个错误,cancel() 将被调用。 然后 operation1 函数将通过 ctx.Done() 被通知并优雅地退出。
你可以通过 ctx.Err() 找到取消的原因。
4. WithTimeout
WithTimeout 允许你指定一个超时时间,如果超过这个时间,就自动取消上下文。
go
func handler() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(
ctx,
3*time.Second,
)
defer cancel()
dataChan := make(chan string)
go databaseQuery(dataChan)
select {
case <- dataChan:
fmt.Println("Query succeeds, do something")
case <- ctx.Done():
fmt.Println("Timeout exceeded, returning")
return
}
}
在上面的例子中,上下文将在三秒后自动取消。
因此,如果在这之前数据库查询没有成功,处理程序将退出并返回。
或者,你也可以通过 cancel() 函数手动取消上下文。
5. WithDeadline
WithDeadline 函数接受一个特定的超时时间,而不是一个持续时间。除此之外,它的工作原理与 WithTimeout 完全相似。
go
func handler() {
ctx := context.Background()
ctx, cancel := context.WithDeadline(
ctx,
time.Now().Add(time.Second*3),
)
defer cancel()
}
上面例子中的上下文将在三秒后自动取消。
五、传递请求范围数据
正如我们通常在函数之间传递 ctx 变量一样,请求范围数据可以使用 WithValue 函数标记该变量。
考虑到一个涉及多个函数调用的应用程序,我们可以通过 ctx 变量将 traceID 传递给这些函数以进行监控和打印日志。
go
const traceIDKey = "trace_id"
func main() {
ctx := context.Background()
ctx = context.WithValue(
ctx,
traceIDKey,
"random_id123",
)
function1(ctx)
}
func function1(ctx context.Context) {
log.Println(
"Entered function 1 with traceID: ",
ctx.Value(traceIDKey),
)
function2(ctx)
}
func function2(ctx context.Context) {
log.Println(
"Entered function 2 with traceID: ",
ctx.Value(traceIDKey),
)
}
WithValue 函数可以将值添加到 ctx 变量中,然后通过 ctx.Value 函数可以读取出来。
六、注意事项和做法
虽然很方便,但 context 经常被误用,并且很容易给你的应用程序带来错误。
在我们结束这篇文章之前,让我们谈谈一些基本的做法。
1. 总是在 defer 中调用 cancel()
css
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
当你通过 WithCancel 函数生成一个新的可取消的 context 时,该模块将:
- 如果调用了 cancel(),将取消事件传播给所有的子 Goroutine
- 跟踪父 context 结构中的所有子 context
如果一个函数在没有调用 cancel() 的情况下返回,Goroutine和 子的 context 将无限期地留在内存中,导致内存泄漏。
这也适用于 WithTimeout 和 WithDeadline,除了这些函数在超过截止日期时自动取消上下文。
但是,对于任何一个函数来说,在 defer 中取消上下文仍然是一个最佳做法。
2. 只对请求范围数据使用 WithValue
go
ctx := context.Background()
ctx1 := context.WithValue(ctx, "key1", "value1")
ctx1 := context.WithValue(ctx1, "key1", "value2")
我们很容易认为,我们是在最后一次函数调用中用 value2 覆盖了 value1。
然而,情况并非如此。
WithValue 函数接收了一个父级上下文并返回了一个副本。因此,它不是覆盖值,而是用一个新的键值对创建一个新的副本。
因此,你应该将 WithValue 的使用限制为有限的请求范围数据。
传递稍后将被改变的参数值将导致创建多个 context 变量,这会导致你的内存使用量大幅增加。
七、总结
关于 context 的讲解就到此为止!
go
type Context interface {
// Channel listen to cancellation
Done() <-chan struct{}
// Return error if context is cancelled
Err() error
// Return the deadline set to the context
Deadline() (deadline time.Time, ok bool)
// Return the value for a given key
Value(key interface{}) interface{}
}
// Return empty root context
func Background() Context
func TODO() Context
// Return cancellable context
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
// Return context with key-value pairs
func WithValue(parent Context, key, val interface{}) Context
上面的要点总结了 context 所提供的所有内容。
希望这篇文章对你有帮助,我们下次再会。
链接:https://juejin.cn/post/7130293077893185544
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。