Context is explained in the following way on the official site.
Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.
https://pkg.go.dev/context
If you want to cancel your processes from the parent, the cancel command somehow needs to be propagated to the children. The Context supports the feature. This post explains how to use context with cancel and timeout. Furthermore, how to implement it if you don’t want to stop the process but want to introduce timeout.
This post contains Goroutine and Channel like the following.
func runChannelWithContext() {
channel := make(chan string) // channel
ctx, cancel := context.WithCancel(context.Background())
go sendWithContext(ctx, channel) // Goroutine
go receiveWithContext(ctx, channel) // Goroutine
time.Sleep(3 * time.Second)
cancel()
time.Sleep(2 * time.Second)
}
Please check the following post, if you don’t know how to use them.
Done: Keep the process running until it receives cancellation
The code looks like this before using context.
import (
"fmt"
"time"
)
const numberOfMsg = 3
func send(channel chan string) {
for i := 0; i < numberOfMsg; i++ {
channel <- fmt.Sprintf("Hello: %d", (i + 1))
time.Sleep(time.Second)
}
}
func receive(channel chan string) {
for {
data := <-channel
fmt.Printf("Received: [%s]\n", data)
}
}
send
function sends data 3 times. The number of messages is defined on numberOfMsg
variable.
Now, we want to remove the limitation.
func sendWithContext(ctx context.Context, channel chan int) {
for i := 0; ; i++ { // numberOfMsg is removed
select {
case <-ctx.Done():
fmt.Println("send loop ends")
return
default:
channel <- fmt.Sprintf("Hello: %d", (i + 1))
time.Sleep(time.Second)
}
}
}
func receiveWithContext(ctx context.Context, channel chan string) {
for {
select {
case data := <-channel:
fmt.Printf("Received: %s\n", data)
case <-ctx.Done():
fmt.Println("receive loop ends")
return
}
}
}
Remember that the Context should be the first parameter. The variable name should be ctx. Context has Done
method that returns Channel. If it’s canceled by someone else, it can receive a notification.
It’s important to write both statements in the select clause. If you implement it in the following way, it doesn’t end the process because data := <-channel
waits until it receives something.
func receiveWithContext(ctx context.Context, channel chan string) {
for {
data := <-channel // wait for the data...
fmt.Printf("Received: %s\n", data)
select {
case <-ctx.Done():
fmt.Println("receive loop ends")
return
}
}
}
By writing them in the select clause, the program processes the data that comes first.
CancelFunc: Cancel the process
It’s easy to cancel the process. There are several methods provided but WithCancel
method is the one that we need to use for the cancellation.
func runChannelWithContext() {
channel := make(chan string)
ctx, cancel := context.WithCancel(context.Background())
go sendWithContext(ctx, channel)
go receiveWithContext(ctx, channel)
time.Sleep(3 * time.Second)
cancel()
time.Sleep(2 * time.Second)
}
It returns a cancel function and if it’s called, the process is notified to the context. The result looks like the following.
$ go run main.go
Received: Hello: 1
Received: Hello: 2
Received: Hello: 3
receive loop ends: context canceled
send loop ends context canceled
If the main event loop is called later, it might be better to call the cancel function with defer
keyword to make sure that the process ends at the end.
func runChannelWithContext() {
channel := make(chan string)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go sendWithContext(ctx, channel)
go receiveWithContext(ctx, channel)
doSomethingForLong() // main event loop
}
Timeout
There are some cases where timeout needs to be implemented as well as the cancellation. context.WithTimeout
can be used in this case.
Its implementation is basically the same as the previous one. Just change the method name and set the timeout duration.
func runChannelWithTimeout() {
channel := make(chan string)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
go sendWithContext(ctx, channel)
go receiveWithContext(ctx, channel)
time.Sleep(5 * time.Second)
cancel()
}
The second parameter for WithTimeout
is the duration for timeout. It’s 2 seconds. So the process ends before cancel
function is called.
$ go run main.go
Received: Hello: 1
Received: Hello: 2
send loop ends context deadline exceeded
receive loop ends: context deadline exceeded
How to differentiate between Timeout and Cancellation
Let’s call the cancel function before the timeout.
func runChannelWithTimeout() {
channel := make(chan string)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
go sendWithContext(ctx, channel)
go receiveWithContext(ctx, channel)
time.Sleep(1 * time.Second)
cancel()
time.Sleep(2 * time.Second)
}
The result is the following.
$ go run main.go
Received: Hello: 1
Received: Hello: 2
receive loop ends: context canceled
send loop ends context canceled
The error message is different.
- Timeout: context deadline exceeded
- Cancel: context canceled
We can use the difference in the Done call. Context package has the corresponding error variables.
- Timeout -> context.DeadlineExceeded
- Cancel -> context.Canceled
So we can use them in the select clause.
func runChannelWithTimeout() {
channel := make(chan string)
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
go sendWithContext(ctx, channel)
// go receiveWithContext(ctx, channel)
go receiveWithTimeout(ctx, channel)
time.Sleep(5 * time.Second)
cancel()
}
func receiveWithTimeout(ctx context.Context, channel chan string) {
for {
select {
case data := <-channel:
fmt.Printf("Received: %s\n", data)
case <-ctx.Done():
err := ctx.Err()
if errors.Is(err, context.Canceled) {
fmt.Println("Canceled")
} else if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Timeout")
}
return
}
}
}
When it’s timeout
$ go run main.go
Received: Hello: 1
Received: Hello: 2
Received: Hello: 3
Timeout
send loop ends context deadline exceeded
When it’s canceled
$ go run main.go
Received: Hello: 1
send loop ends context canceled
Canceled
Keep the process but want to introduce timeout
If you want to keep the process but want to introduce timeout, it needs to be implemented in a different way because once the context is canceled or timeout, the case of ctx.Done() is always executed.
Let’s remove return keyword from the previous version.
func receiveWithTimeout(ctx context.Context, channel chan string) {
for {
select {
case data := <-channel:
fmt.Printf("Received: %s\n", data)
case <-ctx.Done():
err := ctx.Err()
if errors.Is(err, context.Canceled) {
fmt.Println("Canceled")
} else if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Timeout")
}
// return
}
}
}
Then, try to execute it.
$ go run main.go
Received: Hello: 1
Received: Hello: 2
Received: Hello: 3
Timeout
Timeout
Timeout
Timeout
Timeout
...
Tons of Timeout messages are shown in a very short time. This is not what we want.
We want something like that a client sends a request to someone else and consumes the result if it receives the data in the expected time. Otherwise, do something else.
Let’s see the code. It can be implemented it by using time.After(delay)
.
func sendWithContextWithRandomTime(ctx context.Context, channel chan string) {
for i := 0; ; i++ {
select {
case <-ctx.Done():
fmt.Printf("send loop ends %s\n", ctx.Err().Error())
return
default:
channel <- fmt.Sprintf("Hello: %d", (i + 1))
// Sleep duration is random
randomDelay :=time.Duration(rand.Intn(2000)) * time.Millisecond
time.Sleep(randomDelay)
}
}
}
func receiveWithCancelAndTimeout(ctx context.Context, channel chan string) {
for {
select {
case data := <-channel:
fmt.Printf("Received: %s\n", data)
case <-time.After(time.Second):
fmt.Println("timeout")
case <-ctx.Done():
fmt.Printf("receive loop ends: %s\n", ctx.Err().Error())
return
}
}
}
If it receives the data within a second, it consumes the data. If not, timeout is printed.
The result looks like this.
$ go run main.go
Received: Hello: 1
Received: Hello: 2
timeout
Received: Hello: 3
timeout
Received: Hello: 4
Received: Hello: 5
Received: Hello: 6
timeout
receive loop ends: context canceled
send loop ends context canceled
Comments