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 have the above snippet that does print the output like this
I was not canceled
In main, ctx err is context deadline exceeded
Process finished with exit code 0
I understand that context
times-out after 3 seconds and hence it does give me the expected error when I call ctx.Err()
in the end. I also get the fact that in my myfunc
once select
matches on the case for default
, it won't match on the done
. What I do not understand is that how do I make my go func myfunc
get aborted in 3 seconds using the context logic. Basically, it won't terminate in 3 seconds so I am trying to understand how can golang's ctx
help me with this?
If you want to use the timeout and cancellation feature from the context, then in your case the ctx.Done()
need to be handled synchronously.
Explanation from https://golang.org/pkg/context/#Context
Done returns a channel that's closed when work is done on behalf of this context should be canceled. Done may return nil if this context can never be canceled. Successive calls to Done return the same value.
So basically the <-ctx.Done()
will be called on two conditions:
And when that happens, the ctx.Err()
will never be nil
.
We can perform some checking on the error object to see whether the context is canceled by force or exceeding the timeout.
Context package provides two error objects, context.DeadlineExceeded
and context.Timeout
, this two will help us to identify why <-ctx.Done()
is called.
cancel()
)In the test, we'll try to make the context to be canceled before the timeout exceeds, so the <-ctx.Done()
will be executed.
ctx, cancel := context.WithTimeout(
context.Background(),
time.Duration(3*time.Second))
go func(ctx context.Context) {
// simulate a process that takes 2 second 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")
}
}
Output:
$ go run test.go
context cancelled by force
In this scenario, we make the process takes longer than context timeout, so ideally the <-ctx.Done()
will also be executed.
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")
}
}
Output:
$ go run test.go
context timeout exceeded
There might be a situation where we need to stop the goroutine in the middle of the process because error occurred. And sometimes, we might need to retrieve that error object on the main routine.
To achieve that, we need an additional channel to transport the error object from goroutine into main routine.
In the below example, I've prepared a channel called chErr
. Whenever error happens in the middle of (goroutine) process, then we will send that error object through the channel and then stop process immediately from.
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()
right after context initializedAs per context documentation regarding the cancel()
function:
Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete.
It's good to always call cancel()
function right after the context declaration. doesn't matter whether it's also called within the goroutine. This is due to ensure context is always cancelled when the whole process within the block are fully complete.
ctx, cancel := context.WithTimeout(
context.Background(),
time.Duration(3*time.Second))
defer cancel()
// ...
defer cancel()
call within goroutineYou can use defer
on the cancel()
statement within the goroutine (if you want).
// ...
go func(ctx context.Context) {
defer cancel()
// ...
}(ctx)
// ...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With