Golang Context Package Tutorial

What is the Context?

The context package in Go is used to carry around request-scoped values, cancellation signals, and deadlines across API boundaries. It can be used to store metadata, cancelation signals, timeouts, and other request-scoped values. The context package provides a way to cancel long-running operations, and to store metadata across API boundaries. It is often used with the http package to manage request-scoped values and cancelation signals for HTTP requests.

It allows you to propagate request-scoped values across multiple function calls and goroutines, making it easier to manage the flow of information in your application.

A context.Context value is created using the context.With* functions, such as context.WithValue, context.WithCancel, or context.WithTimeout. These functions return a new context.Context value that carries the specified values or signals.

The context.Context value can be passed as an argument to functions and methods that need to access request-scoped values or listen for cancellation signals. These functions can then use the context.Value and context.Done methods to access the values and signals stored in the context.

The context package is especially useful in situations where you need to cancel a long-running operation or propagate request-scoped values across multiple goroutines. It's commonly used in server-side programming and other concurrent scenarios.

You should always pass context as the first argument to any function that performs work that might be cancelled.

For example, a HTTP server can use context to cancel a request's work when the client disconnects, a database package can use context to implement a cancelable query, and so on.

The context package defines the Context type, which is a Go interface with four methods, named Deadline(), Done(), Err(), and Value():

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() 

The Methods Defined by the Context Interface

NameDescription
Value(key) This method returns the value associated with the specified key.
Done() This method returns a channel that can be used to receive a cancelation notification.
Deadline() This method returns the time.Time that represents the deadline for the request and a bool value that will be false if no deadline has been specified.
Err() This method returns an error that indicates why the Done channel received a signal. The context package defines two variables that can be used to compare the error: Canceled indicates that the request was canceled, and DeadlineExeeded indicates that the deadline passed.

The context Package Functions for Creating Context Values

NameDescription
Background() This method returns the default Context, from which other contexts are derived.
WithCancel(ctx) This method returns a context and a cancelation function.
WithDeadline(ctx, time) This method returns a context with a deadline, which is expressed using a time.Time value.
WithTimeout(ctx, duration) This method returns a context with a deadline, which is expressed using a time.Duration value.
WithValue(ctx, key, val) This method returns a context containing the specified key-value pair.


Give an example of context.WithCancel

Here's an example of how context.WithCancel can be used in Go:

Example

package main

    import (
        "context"
        "fmt"
    )
    
    func doWork(ctx context.Context) {
        for {
            select {
            case <-ctx.Done():
                fmt.Println("Work done!")
                return
            default:
                fmt.Println("Working...")
            }
        }
    }
    
    func main() {
        ctx, cancel := context.WithCancel(context.Background())
        defer cancel() // cancel when we are finished
    
        go doWork(ctx)
    
        // Wait for a while before canceling the context
        select {
        case <-ctx.Done():
        case <-time.After(time.Second * 3):
            cancel()
        }
    }

In this example, we create a new context ctx using context.WithCancel(context.Background()). The context.Background() function returns an empty background context. context.WithCancel returns a new context and a cancel function. We defer the cancel function so that it is called when the main function exits. In the doWork function, it will check if the context has been done, if yes, the function will return.

In the main function, we are running a goroutine and doing the work in it by passing the context. After 3 seconds of waiting, the main function will cancel the context by calling the cancel function, which will make the context's Done channel be closed. As a result, the doWork function will receive from the Done channel, print "Work done!" and return.


Give an example of context.WithTimeout

In Go, you can use the context.WithTimeout function to create a new context that is canceled when the specified timeout elapses. The function takes two arguments: an existing context and a duration for the timeout.

Here is an example of how to use context.WithTimeout to create a context that is canceled after 5 seconds:

Example

package main

    import (
        "context"
        "fmt"
        "time"
    )
    
    func main() {
        ctx := context.Background()
        ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
        defer cancel()
    
        // Do some work
        select {
        case <-ctx.Done():
            fmt.Println("Work completed")
        case <-time.After(10 * time.Second):
            fmt.Println("Work took longer than 10 seconds")
        }
    }

In this example, the context is created by calling context.WithTimeout with the background context and a timeout of 5 seconds. The function returns a new context and a function to cancel the context. The cancel function is called in a defer statement to ensure that the context is canceled when the function returns. The select statement is used to wait for the context to be done or for the timeout to elapse.

You can also check if the context is done by using the ctx.Done() channel, you can use this channel in a select statement or in a loop to check if the context is done, if it's done it means that the context is expired.


Give an example of context.WithDeadline

In Go, the context.WithDeadline function creates a new context with an associated deadline. The deadline is a specific point in time after which the context will be considered "dead" and any associated work will be cancelled. The function takes in two arguments: the existing context, and the deadline time. It returns a new context that will be cancelled at the specified deadline.

Here is an example:

Example

package main

    import (
        "context"
        "fmt"
        "time"
    )
    
    func main() {
        ctx := context.Background()
        deadline := time.Now().Add(time.Second * 5)
        ctx, cancel := context.WithDeadline(ctx, deadline)
        defer cancel()
    
        select {
        case <-time.After(time.Second * 10):
            fmt.Println("overslept")
        case <-ctx.Done():
            fmt.Println(ctx.Err())
        }
    }

In this example, a background context is created, and then a deadline is set 5 seconds in the future. The WithDeadline function is used to create a new context based on the background context, with the specified deadline. The select statement is used to wait for either the context to be cancelled or 10 seconds to pass. If the context is cancelled before 10 seconds, it will print the error message context deadline exceeded, otherwise it will print "overslept"


SQL Query Timeout using Context

To use SQL queries with a timeout in Golang, you can use the context package to set a deadline for the query execution. First, create a context with a timeout using the context.WithTimeout function. Then, pass the context as the first argument to the query execution function (such as db.QueryContext() or db.ExecContext()).

Here is an example of how to set a timeout of 1 second for a SELECT query:

Example

package main

    import (
      "context"
      "database/sql"
      "fmt"
      "time"
    )
    
    func main() {
      // Open a connection to the database
      db, _ := sql.Open("driverName", "dataSourceName")
    
      // Create a context with a timeout of 1 second
      ctx, cancel := context.WithTimeout(context.Background(), time.Second)
      defer cancel()
    
      // Execute the query with the context
      rows, err := db.QueryContext(ctx, "SELECT * FROM table")
      if err != nil {
        fmt.Println(err)
      }
      defer rows.Close()
    
      // Handle the query results
      // ...
    }

If the query takes longer than 1 second to execute, it will be cancelled and an error will be returned.


Read file with Context Timeout

In Go, you can use the context package to set a timeout for reading a file. Here is an example:

Example

package main

    import (
        "context"
        "fmt"
        "io/ioutil"
        "time"
    )
    
    func main() {
        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
        defer cancel()
    
        data, err := ioutil.ReadFile("example.txt", ctx)
        if err != nil {
            fmt.Println("Error:", err)
            return
        }
    
        fmt.Println(string(data))
    }

In this example, we first create a context with a timeout of 2 seconds using context.WithTimeout. We then pass this context to ioutil.ReadFile to read the contents of the file "example.txt". If the file takes more than 2 seconds to read, the context's Done channel is closed, and ioutil.ReadFile returns an error.


Using Context for HTTP

In Go, you can use the context package to set a timeout for an HTTP request. Here is an example:

Example

package main

    import (
        "context"
        "fmt"
        "io/ioutil"
        "net/http"
        "time"
    )
    
    func main() {
        ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
        defer cancel()
    
        req, err := http.NewRequest("GET", "https://example.com", nil)
        if err != nil {
            fmt.Println("Error:", err)
            return
        }
        req = req.WithContext(ctx)
    
        client := &http.Client{}
        resp, err := client.Do(req)
        if err != nil {
            fmt.Println("Error:", err)
            return
        }
        defer resp.Body.Close()
    
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            fmt.Println("Error:", err)
            return
        }
    
        fmt.Println(string(body))
    }

In this example, we first create a context with a timeout of 2 seconds using context.WithTimeout. We then attach this context to the HTTP request using req.WithContext(ctx). When we make the request using the http.Client.Do method, it will automatically cancel the request if the context's Done channel is closed before a response is received.

You could also use client libraries like golang.org/x/net/context/ctxhttp to make http request with context, this library has a Get and Post method that takes context as a first argument and returns response and error.

Example

package main

  import (
      "context"
      "fmt"
      "io/ioutil"
      "net/http"
      "time"
  
      "golang.org/x/net/context/ctxhttp"
  )
  
  func main() {
      ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
      defer cancel()
  
      resp, err := ctxhttp.Get(ctx, nil, "https://example.com")
      if err != nil {
          fmt.Println("Error:", err)
          return
      }
      defer resp.Body.Close()
  
      body, err := ioutil.ReadAll(resp.Body)
      if err != nil {
          fmt.Println("Error:", err)
          return
      }
  
      fmt.Println(string(body))
  }

This example uses the ctxhttp.Get method to make a GET request to "https://example.com" with a timeout of 2 seconds.

It's important to note that in both cases, the request will be canceled if the Done channel is closed by the context, but it will not close the connection. It's the responsibility of the application to close the connection.


Using Contexts as key-value stores

In Go, you can use the context package to store key-value pairs of data that can be passed along with a request or a piece of code. This allows you to associate additional information with a request or piece of code, without having to pass it as an explicit argument. Here is an example:

Example

package main

    import (
        "context"
        "fmt"
    )
    
    func main() {
        ctx := context.WithValue(context.Background(), "user_id", "12345")
        // use the context in a function
        processRequest(ctx)
    }
    
    func processRequest(ctx context.Context) {
        userID := ctx.Value("user_id").(string)
        fmt.Println("User ID:", userID)
    }

In this example, we first create a context using context.WithValue method. We pass context.Background() as the parent context, and a key-value pair of "user_id" and "12345" to the method. Then we pass this context to the processRequest function. In the function, we use the Value method to retrieve the "user_id" value from the context, and print it.

It's important to note that context values are only meant to be used for request-scoped data that transits processes and API boundaries, not for passing optional parameters to functions.If you need to pass optional parameters to a function, it's better to use structs or functional options pattern.

Also, context values are not thread-safe, if you are in a concurrent environment, use a sync.Map or equivalent.


Context Package Best Practices

There are several best practices for using the context package in Go:

  • Use context.WithCancel, context.WithTimeout, or context.WithDeadline to create a context with a timeout or cancellation signal.
  • Always pass the context as the first argument to functions that might take a long time to complete, such as network requests or database queries.
  • Use context.Value to store and retrieve values associated with a context, such as a user ID or request ID.
  • Use context.WithValue to create a new context based on an existing context and associate additional values with it.
  • Check the Done channel of a context to see if it has been canceled.
  • Use the context package throughout your application to propagate request-scoped values and cancellation signals, rather than using global variables or manual signaling.
  • Avoid using context.Background() as it has no timeout or cancellation signal, instead use context.TODO() to indicate that the context will be replaced by the caller later.
  • Do not store contexts in structs, instead pass them as arguments to functions.
  • Always check the error return value of context-aware functions to see if the context was canceled or timed out.