Controlling code with golang context

5 min read

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

zen8labs controlling code with golang context 1

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. 

zen8labs controlling code with golang context 2
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

https://medium.com/@jamal.kaksouri/the-complete-guide-to-context-in-golang-efficient-concurrency-management-43d722f6eaea

Linh Dinh, Software Engineer

Related posts

Scaling a web application transcends mere technical challenges - In our latest blog we look at how to improve the system design of a website to allow us to go from zero users to millions of users in a professional manner.
7 min read
The world today is filled with a litany of screens ranging in all sizes. Our latest blog looks at Responsive CSS: Media Queries to understand the challenges of them and how to meet the right size
4 min read
Serving static files is an important part of deploying a web application. At zen8labs we use a variety of platforms such as Django, in our latest blog, look at some of the basic steps and essential considerations that can help you.
3 min read