In programming, the controlling code is in my opinion the most important. There are 4 contexts as to what is controlling code ranging from programming context to automated systems. But what happens when you don’t know if a function is executed completely or not? How can you stop a job which is taking a longer time than you think? How do you stop a request when a client’s connection is disconnected, or even cancel a database query? We shall have a look to golang as it provides powerful context package with functions to control cancellation.
Prerequisites
Before going to main part, we should have a glance at goroutine.
A goroutine is a lightweight thread managed by the Go runtime. Goroutine is used to concurrently handle tasks. Using goroutine with a channel will allow communication between goroutines. By taking advantage of this technique, we can control cancellation of goroutine.
package main
import (
"fmt"
"time"
)
func count(c chan int) {
n := 0
for {
select {
case <-c:
return
default:
fmt.Println(n)
n++
time.Sleep(time.Second)
}
}
}
func main() {
c := make(chan int)
go count(c)
time.Sleep(5 * time.Second)
c <- 1
}
In the example, the count function prints a number to which one is added after every second, while the for loop checks the c channel at the same time. If the c channel has value, the count function will be terminated. The count function will run forever if the c channel has no value. In the main function after 5 seconds a value is passed to the c channel. As a result, after 5 seconds the count function will receive value for the c channel, the code return is executed to terminate the count function.
By applying the mechanism of goroutine and channel, the context package is created to make it easier. The out of the box functions in this package are powerful and easy to use. In this post, I will try to show you how it works in the golang world, in the easiest way possible.
Context with timeout
Functions context.WithTimeout()
and context.WithDeadline()
allow you to cancel a process after a specified time. For example, you have a long-running task, you want to control the time it works. If it takes more than 5 seconds you need to terminate it.
package main
import (
"context"
"fmt"
"time"
)
func performLongRunningTask(ctx context.Context) {
for {
fmt.Println("working...")
time.Sleep(time.Second)
}
}
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
go performLongRunningTask(ctx)
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
}
}
https://go.dev/play/p/i2l_-ikTihB
In the example above, we create empty context with context.Background()
. Then context.WithTimeout()
needs two params, one is a context, one is a duration which context needs to wait for before a cancellation signal is called. The performLongRunningTask()
function is used to simulate a long-running task which prints a text “working…” every second. The long-running task is run in a goroutine (the mode allows a function to run concurrently). We can recognize that the task will run forever. The piece of code “select case” plays the role of waiting for the cancellation signal. When a cancellation signal is emitted by timeout. The signal is a channel created and ctx.Done()
will receive the value of that channel. As a result, fmt.Println(ctx.Err())
is triggered, and ctx.Err()
returns the reason why the signal is emitted. Finally, the main function is end, the consequence of this is performLongRunningTask()
in the goroutine is also terminated.
It should be noted that, you need to be aware that context will be cancelled when it is passed to a gorountine. The reason is the main gorountine done before the gorountine with context is executed completely.
In addition, by propagating the signal via channel we can terminate a process in goroutine in an easy way. The example below, we create a count function and print it every second. After 5 seconds.
package main
import (
"context"
"fmt"
"time"
)
func count(ctx context.Context) {
n := 0
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err())
return
default:
fmt.Println(n)
n++
time.Sleep(time.Second)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go count(ctx)
time.Sleep(6 * time.Second)
}
https://go.dev/play/p/Fwlr-dk4EGq
We can see that the cancellation signal is emitted and the ctx.Done() function inside the count function will reach the signal then terminate the process in goroutine.
Context with deadline
The context.WithDeadline function possesses the analogy with context.WithTimeout. There is only a small difference which is the param passed instead of the duration, it is a point of time in future.
package main
import (
"context"
"fmt"
"time"
)
func performTask(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("Task completed or deadline exceeded:", ctx.Err())
return
}
}
func main() {
ctx, cancel := context.WithDeadline(context.Background(),
time.Now().Add(2*time.Second))
defer cancel()
go performTask(ctx)
time.Sleep(3 * time.Second)
}
We can see that the second returning param of context.WithDeadline is the point of time after 2 seconds from now. The rest of the example performs a feature which is similar to former examples.
Context with cancel
golang also provides a method more flexible than context.WithDeadline()
and context.WithTimeout()
are. It is context.WithCancel()
, this method helps you spread the cancellation signal in any cases that you want by calling cancel()
function.
package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
go performTask(ctx)
time.Sleep(2 * time.Second)
cancel()
time.Sleep(1 * time.Second)
}
func performTask(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Task cancelled")
return
default:
// Perform task operation
fmt.Println("Performing task...")
time.Sleep(500 * time.Millisecond)
}
}
}
We can see the cancel() function called after 2 seconds, this is the time that cancel() sends the signal to ctx.Done() in performTask() to terminate it. The context.WithCancel() is more flexible when you want to emit the signal inside your logic code.
Context in HTTP request
With the things mentioned above, we can grasp the vital role of context in a golang project. HTTP request is empowered to treat requests with context. Now, you can cancel the request whenever you want.
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://api.example.com/data", nil)
if err != nil {
fmt.Println("Error creating request:", err)
return
}
client := http.DefaultClient
r.esp, err := client.Do(req)
if err != nil {
fmt.Println("Error making request:", err)
return
}
defer resp.Body.Close()
}
The example set context with 2 second timeout. If the HTTP request takes more than 2 seconds, it will be canceled. the way equips developers with controlling resources effectively.
Context in Database Operations
Context also combines with the database operation, which is equipped with cancellation. You can make sure that your code cancels a query if it takes too long a time to handle.
package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "github.com/lib/pq"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
db, err := sql.Open("postgres", "postgres://username:password@localhost/mydatabase?sslmode=disable")
if err != nil {
fmt.Println("Error connecting to the database:", err)
return
}
defer db.Close()
rows, err := db.QueryContext(ctx, "SELECT * FROM users")
if err != nil {
fmt.Println("Error executing query:", err)
return
}
defer rows.Close()
// Process query results
}
Conclusion
In conclusion, we have learnt why the controlling code is an important function and what goroutine is, the codes above are also there to help guide you through along the path of controlling code. I hope you can grasp the importance of context in golang, which we can see in every golang project. My co-workers at zen8labs have experience with golang and all coding languages, check it at for yourself!
References
Linh Dinh, Software Engineer