package main
import (
"context"
"fmt"
"sync"
"time"
)
func myfunc(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Printf("Ctx is kicking in with error:%+v\n", ctx.Err())
return
default:
time.Sleep(15 * time.Second)
fmt.Printf("I was not canceled\n")
return
}
}
}
func main() {
ctx, cancel := context.WithTimeout(
context.Background(),
time.Duration(3*time.Second))
defer cancel()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
myfunc(ctx)
}()
wg.Wait()
fmt.Printf("In main, ctx err is %+v\n", ctx.Err())
}
我有上面的代码片段,可以像这样打印输出
I was not canceled
In main, ctx err is context deadline exceeded
Process finished with exit code 0
我知道
context
在 3 秒后超时,因此当我最后调用 ctx.Err()
时,它确实给了我预期的错误。我还了解到,在我的 myfunc
中,一旦 select
与 default
的情况匹配,它就不会与 done
匹配。我不明白的是,如何使用上下文逻辑让我的 go func myfunc
在 3 秒内中止。基本上,它不会在 3 秒内终止,所以我想了解 golang 的 ctx
如何帮助我解决这个问题?
如果您想在上下文中使用超时和取消功能,那么在您的情况下需要同步处理
ctx.Done()
。
来自 https://golang.org/pkg/context/#Context 的解释
Done 返回一个通道,当代表此上下文的工作完成时,该通道关闭。如果此上下文永远无法取消,则 Done 可能会返回 nil。连续调用 Done 返回相同的值。
所以基本上
<-ctx.Done()
将在两个条件下被调用:
当这种情况发生时,
ctx.Err()
将永远不会是nil
。
我们可以对错误对象进行一些检查,看看上下文是否被强制取消或超过超时。
Context 包提供了两个错误对象,
context.DeadlineExceeded
和context.Timeout
,这两个将帮助我们识别为什么调用<-ctx.Done()
。
cancel()
)在测试中,我们会尝试在超时之前取消上下文,这样
<-ctx.Done()
就会被执行。
ctx, cancel := context.WithTimeout(
context.Background(),
time.Duration(3*time.Second))
go func(ctx context.Context) {
// simulate a process that takes 2 seconds to complete
time.Sleep(2 * time.Second)
// cancel context by force, assuming the whole process is complete
cancel()
}(ctx)
select {
case <-ctx.Done():
switch ctx.Err() {
case context.DeadlineExceeded:
fmt.Println("context timeout exceeded")
case context.Canceled:
fmt.Println("context cancelled by force. whole process is complete")
}
}
输出:
$ go run test.go
context cancelled by force
在这种情况下,我们使进程花费的时间比上下文超时时间更长,因此理想情况下
<-ctx.Done()
也将被执行。
ctx, cancel := context.WithTimeout(
context.Background(),
time.Duration(3*time.Second))
go func(ctx context.Context) {
// simulate a process that takes 4 second to complete
time.Sleep(4 * time.Second)
// cancel context by force, assuming the whole process is complete
cancel()
}(ctx)
select {
case <-ctx.Done():
switch ctx.Err() {
case context.DeadlineExceeded:
fmt.Println("context timeout exceeded")
case context.Canceled:
fmt.Println("context cancelled by force. whole process is complete")
}
}
输出:
$ go run test.go
context timeout exceeded
可能有一种情况,我们需要在进程中停止 goroutine,因为发生了错误。有时,我们可能需要在主例程中检索该错误对象。
为了实现这一点,我们需要一个额外的通道来将错误对象从 goroutine 传输到主例程中。
在下面的示例中,我准备了一个名为
chErr
的通道。每当(goroutine)进程中间发生错误时,我们将通过通道发送该错误对象,然后立即停止进程。
ctx, cancel := context.WithTimeout(
context.Background(),
time.Duration(3*time.Second))
chErr := make(chan error)
go func(ctx context.Context) {
// ... some process ...
if err != nil {
// cancel context by force, an error occurred
chErr <- err
return
}
// ... some other process ...
// cancel context by force, assuming the whole process is complete
cancel()
}(ctx)
select {
case <-ctx.Done():
switch ctx.Err() {
case context.DeadlineExceeded:
fmt.Println("context timeout exceeded")
case context.Canceled:
fmt.Println("context cancelled by force. whole process is complete")
}
case err := <-chErr:
fmt.Println("process fail causing by some error:", err.Error())
}
cancel()
根据关于 cancel()
功能的
上下文文档:
取消此上下文会释放与其关联的资源,因此在此上下文中运行的操作完成后,代码应立即调用取消。
最好总是在上下文声明之后立即调用
cancel()
函数。它是否也在 goroutine 中被调用并不重要。这是为了确保当块内的整个过程完全完成时,上下文始终被取消。
ctx, cancel := context.WithTimeout(
context.Background(),
time.Duration(3*time.Second))
defer cancel()
// ...
defer cancel()
在goroutine中调用您可以在 goroutine 中的
defer
语句上使用 cancel()
(如果需要)。
// ...
go func(ctx context.Context) {
defer cancel()
// ...
}(ctx)
// ...
在您的
for ... select
中,您有 2 个案例:case <-ctx.Done():
和 default:
。当您的代码到达 select
时,它会进入 default
情况,因为上下文尚未取消,它会休眠 15 秒然后返回,从而打破循环。 (换句话说,它不会阻塞/等待您的上下文取消)
如果您希望代码执行您所描述的操作,则需要
select
具备取消上下文的情况 以及 强制超时。
select {
case <-ctx.Done(): // context was cancelled
fmt.Printf("Ctx is kicking in with error:%+v\n", ctx.Err())
return
case <-time.After(15 * time.Second): // 15 seconds have elapsed
fmt.Printf("I was not canceled\n")
return
}
现在,您的代码将在遇到
select
时阻塞,而不是进入 default
情况并中断循环。