Understanding the Context Package for Managing Goroutines in Go

This documentation provides a comprehensive guide to the Context package in Go, which is crucial for managing goroutines effectively. It covers the basics of goroutines, concurrency concepts, and the essential use cases of the Context package.

In the world of concurrent programming, Go offers a powerful yet elegant way to handle goroutines through the context package. This documentation will guide you through understanding what goroutines are, the basics of concurrency in Go, and how the context package can be used to manage goroutines effectively.

Introduction to Goroutines

What are Goroutines?

Goroutines are lightweight threads managed by the Go runtime. They are the building blocks for writing concurrent programs in Go. Think of goroutines as a group of workers performing tasks independently but within the same program. Just like how different departments in a factory work on different tasks but contribute to the overall production, goroutines work independently on different tasks but form a single application.

Starting a Goroutine

To start a goroutine in Go, you simply prepend the go keyword before a function call. Here's a simple example:

package main

import (
    "fmt"
    "time"
)

func say(message string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(message)
    }
}

func main() {
    go say("world")
    say("hello")
}

In this example, the say function is started as a goroutine with the go keyword. This function prints the message five times with a short delay between each print. The main function also calls say directly. Because goroutines run concurrently, they can execute at the same time, leading to an interleaved output of "hello" and "world".

Characteristics of Goroutines

  • Lightweight: Goroutines are much cheaper than traditional threads, requiring only a small stack space which can grow and shrink as needed.
  • Concurrency: Multiple goroutines can run concurrently within the same program, making it easy to write programs that perform multiple tasks simultaneously.
  • Simplicity: Goroutines are straightforward to use. They feel like regular functions, requiring no special code or libraries.

Introduction to Concurrency in Go

Concurrency Basics

Concurrency in Go allows a program to execute multiple operations simultaneously. This is particularly useful for I/O bound and network applications where waiting for external resources is common. Think of concurrency as different cooks preparing different dishes in a kitchen. They don’t wait for each other to complete their task; instead, they work independently and contribute to the grand meal.

Parallelism and Concurrency: Differences

  • Concurrency: Concurrency is about handling multiple tasks at different times, not necessarily at the exact same instant. In Go, concurrency is achieved through goroutines.
  • Parallelism: Parallelism is about executing tasks simultaneously. It depends on multiple CPU cores being available, which allows real simultaneous execution of multiple tasks.

Go's Concurrency Model

Go's concurrency model, known as the CSP (Communicating Sequential Processes) model, is built around communication between goroutines using channels. The concurrency model enables writing simple concurrent programs by composing independent, thread-safe components.

The Need for Managing Goroutines

Challenges Without Management

When working with goroutines, several challenges can arise. For example, if you start too many goroutines, you may exhaust system resources. There’s also the issue of coordinating the lifecycle of goroutines, such as safely stopping them when done or when an error occurs, and ensuring that they don’t leak or run indefinitely, leading to resource wastage.

Importance of Proper Management

Proper management of goroutines is essential for building robust and efficient concurrent applications. This includes cleaning up resources after goroutines finish their tasks, handling timeouts, and propagating errors and cancellation signals effectively.

Introduction to the Context Package

What is the Context Package?

The context package provides a way to manage the lifecycle of goroutines, coordinate work, and cleanly propagate cancellation or timeout signals. It is a powerful tool that helps in building reliable and maintainable concurrent systems.

Purpose of the Context Package

The primary purposes of the context package are:

  1. Propagation: Propagating cancellation signals, deadlines, and values across different parts of a program.
  2. Cancellation: Canceling goroutines when they are no longer needed.
  3. Timeouts and Deadlines: Allowing goroutines to complete within a specified time frame.
  4. Values: Passing request-scoped data to functions without needing to rewrite your functions to accept every bit of data your application needs.

Using Context to Manage Goroutines

Creating a Context

There are several ways to create a context, depending on the requirements of your application.

Background Context

A background context can be created using context.Background(). It is typically used as a root context.

package main

import (
    "context"
    "fmt"
    "time"
)

func main() {
    ctx := context.Background()
    fmt.Println("Created a background context:", ctx)
}

This example creates a background context and prints it. The context itself doesn’t do anything, but it is a starting point for deriving other contexts.

WithCancel

The WithCancel function is used to create a derived context from an existing context. This derived context can be canceled, and any goroutine using this context can be notified of the cancellation.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(3 * time.Second)
    fmt.Println("Stopping workers...")
    cancel()
    time.Sleep(1 * time.Second) // wait to let workers clean up
}

In this example, two workers are started as goroutines. Each worker checks the ctx.Done() channel in a select statement. When cancel() is called, the Done() channel is closed, and the workers receive the cancellation signal, which they handle by stopping.

WithDeadline

The WithDeadline function creates a derived context that is canceled once the deadline is exceeded.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    deadline := time.Now().Add(3 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, the context is set to be canceled within 3 seconds. The workers check the Done() channel and stop when the deadline is exceeded.

WithTimeout

The WithTimeout function is similar to WithDeadline but specifies the duration instead of the exact deadline.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, the context will be canceled after 3 seconds. The workers will receive the cancellation signal and stop working.

WithValue

The WithValue function allows you to attach key-value pairs to a context. This is useful for passing request-scoped data around, such as request IDs or authentication tokens.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    value := ctx.Value("requestID")
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d with requestID %v stopping\n", id, value)
            return
        default:
            fmt.Printf("Worker %d with requestID %v working...\n", id, value)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx := context.WithValue(context.Background(), "requestID", "12345")
    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(3 * time.Second)
}

In this example, a context is created with a key-value pair for a request ID. This value is passed to the worker functions, which then use the request ID for their tasks.

Canceling a Context

Using WithCancel

Canceling a context is crucial for safely stopping goroutines. The WithCancel function provides a cancel function that can be called to signal cancellation.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(3 * time.Second)
    fmt.Println("Stopping workers...")
    cancel()
    time.Sleep(1 * time.Second) // wait to let workers clean up
}

In this example, workers run concurrently until the cancel function is called. The workers check the Done() channel to see if the context has been canceled and stop themselves if necessary.

Graceful Shutdown

Graceful shutdown means stopping goroutines in a controlled manner to ensure no work is lost. Here’s how to implement a graceful shutdown using the context package.

package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping...\n", id)
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go worker(ctx, 1)
    go worker(ctx, 2)

    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

    fmt.Println("Waiting for termination signal...")
    <-sig
    fmt.Println("Received termination signal, canceling context...")
    cancel()
    time.Sleep(1 * time.Second) // wait to let workers clean up
    fmt.Println("Exiting...")
}

In this example, the program listens for termination signals (like SIGINT or SIGTERM) from the operating system. When such a signal is received, the context is canceled, and the workers stop gracefully.

Setting and Using Timeouts and Deadlines

Using WithTimeout

Setting timeouts ensures that goroutines do not run indefinitely. The WithTimeout function creates a context that is canceled after a specified duration.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, workers run for up to 3 seconds before being canceled automatically. The defer cancel() statement ensures that the context is canceled when the main function exits, even if it exits early.

Using WithDeadline

The WithDeadline function creates a context that is canceled at a specific time.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    deadline := time.Now().Add(3 * time.Second)
    ctx, cancel := context.WithDeadline(context.Background(), deadline)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, workers run until the deadline specified is reached, at which point the context is canceled.

Handling Timeouts

Timeouts and deadlines are critical for ensuring that goroutines do not run indefinitely and to free up resources when a timeout occurs. Here’s how to handle timeouts effectively.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, workers run until the timeout is reached. The ctx.Err() method returns the reason why the context was canceled.

Passing Values Through Context

Using WithValue

The WithValue function allows you to attach key-value pairs to a context, which can be retrieved elsewhere in the application.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    requestID := ctx.Value("requestID")
    fmt.Printf("Worker %d started with requestID %s\n", id, requestID)
    time.Sleep(2 * time.Second)
    fmt.Printf("Worker %d completed with requestID %s\n", id, requestID)
}

func main() {
    ctx := context.WithValue(context.Background(), "requestID", "12345")
    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, a request ID is attached to the context and passed to the worker functions, which then use it for their tasks.

Accessing Values

Values stored in a context can be accessed using the ctx.Value(key) function.

package main

import (
    "context"
    "fmt"
)

func worker(ctx context.Context, id int) {
    requestID := ctx.Value("requestID")
    fmt.Printf("Worker %d started with requestID %s\n", id, requestID)
    fmt.Printf("Worker %d completed with requestID %s\n", id, requestID)
}

func main() {
    ctx := context.WithValue(context.Background(), "requestID", "12345")
    go worker(ctx, 1)
    go worker(ctx, 2)
    time.Sleep(1 * time.Second)
}

In this example, the worker functions access the request ID from the context and use it for their work.

Best Practices

  • Do not store large values: Contexts should be lightweight. Avoid storing large values or values that can grow indefinitely.
  • Use keys appropriately: Use keys of custom types to avoid collisions.
  • Avoid global contexts: Avoid using global contexts, as they can lead to memory leaks if not managed properly.

Propagation and Derivation of Contexts

Deriving Contexts

Contexts can be derived from a parent context using functions like WithCancel, WithDeadline, WithTimeout, and WithValue. Deriving a context inherits properties from its parent.

Propagating Contexts Across Goroutines

It is a best practice to pass contexts to all functions that accept a context. This ensures that cancellation signals and timeouts propagate correctly.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    requestID := ctx.Value("requestID")
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Printf("Worker %d with requestID %v working...\n", id, requestID)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx := context.WithValue(context.Background(), "requestID", "12345")
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, a context with a timeout is created, and a request ID is attached. Both the worker functions receive the context and use the request ID for their tasks. The context is canceled after 3 seconds.

Best Practices for Context Propagation

  • Pass context as the first argument: When passing a context to a function, pass it as the first parameter.
  • Do not pass context through the global variables: Avoid global contexts, as they can lead to memory leaks.
  • Cancel contexts when they are no longer needed: Use defer cancel() to ensure that contexts are canceled when they are no longer needed.

Handling Cancellation and Timeout Signals

Receiving Cancellation Signals

To receive cancellation signals, goroutines need to listen to the Done() channel of the context.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, workers check the Done() channel to receive cancellation signals and stop their work when the channel is closed.

Handling Timeout Signals

Timeouts are handled by the WithTimeout function, which creates a context with a specified duration. When the duration is exceeded, the context is canceled.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, the workers are canceled after 3 seconds, as specified by the WithTimeout function.

Using Select to Handle Multiple Signals

The select statement can be used to handle multiple signals concurrently, such as cancellation and timeout signals.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, the select statement is used to listen to the Done() channel for cancellation signals. The workers are canceled after 3 seconds.

Best Practices for Using Context in Goroutines

Context in API Calls

When making API calls, it is a good practice to pass the context to the call and handle cancellation signals appropriately.

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

func fetch(ctx context.Context, url string) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    client := http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Request canceled:", err)
        return
    }
    defer resp.Body.Close()
    fmt.Println("Request completed:", resp.Status)
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go fetch(ctx, "https://jsonplaceholder.typicode.com/todos/1")
    time.Sleep(5 * time.Second)
}

In this example, an HTTP request is made with a context. The request will be automatically canceled if it takes longer than 3 seconds.

Context in Libraries

Libraries should accept a context in functions that perform I/O operations to ensure they can be canceled.

package main

import (
    "context"
    "fmt"
    "time"
)

func process(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Processing canceled")
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Println("Processing...")
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go process(ctx)
    time.Sleep(5 * time.Second)
}

In this example, a processing function is called with a context that is canceled after 3 seconds.

Preventing Resource Leaks

Always cancel contexts when they are no longer needed to prevent resource leaks. Use defer cancel() to ensure contexts are canceled, even if a function exits early.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, the context is canceled after 3 seconds, ensuring that workers stop and system resources are freed up.

Handling Cancellation and Timeout Signals

Receiving Cancellation Signals

To handle cancellation signals, listen to the Done() channel of the context.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, the workers stop once the context is canceled after 3 seconds.

Handling Timeout Signals

Timeouts are handled by the WithTimeout function, which creates a context with a specified duration. If the duration is exceeded, the context is canceled.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, workers are stopped after 3 seconds due to the timeout.

Using Select to Handle Multiple Signals

The select statement can handle multiple signals concurrently.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, the select statement is used to handle the cancellation signal from the context.

Best Practices for Using Context in Goroutines

Context in API Calls

Always pass a context to functions that make API calls to ensure that they can be canceled.

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

func fetch(ctx context.Context, url string) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    client := http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Request canceled:", err)
        return
    }
    defer resp.Body.Close()
    fmt.Println("Request completed:", resp.Status)
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go fetch(ctx, "https://jsonplaceholder.typicode.com/todos/1")
    go fetch(ctx, "https://jsonplaceholder.typicode.com/todos/2")

    time.Sleep(5 * time.Second)
}

In this example, HTTP requests are made with a context, which is canceled after 3 seconds.

Context in Libraries

Libraries should accept a context as a parameter to ensure they can be canceled or timed out.

package main

import (
    "context"
    "fmt"
    "time"
)

func process(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Processing canceled")
            fmt.Println(ctx.Err())
            return
        default:
            fmt.Println("Processing...")
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go process(ctx)
    time.Sleep(5 * time.Second)
}

In this example, the processing function accepts a context and stops when the timeout is reached.

Preventing Resource Leaks

Cancel contexts to prevent resource leaks.

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d stopping\n", id)
            return
        default:
            fmt.Printf("Worker %d working...\n", id)
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go worker(ctx, 1)
    go worker(ctx, 2)

    time.Sleep(5 * time.Second)
}

In this example, the context is canceled after 3 seconds, ensuring that workers stop and resources are freed up.

Common Pitfalls and Mistakes

Incorrect Context Usage

Incorrectly using contexts can lead to issues such as goroutines running indefinitely or unnecessary resource usage.

Overusing Context Values

Avoid attaching too many values to a context. Too many values can lead to a bloated context and make the code difficult to manage.

Mispropagating Context

Ensure that contexts are propagated correctly to all functions that need them. Failing to do so can result in goroutines that do not receive cancellation signals.

Real-world Applications of Context

Example: Managing Database Query Timeouts

Timeouts are essential when dealing with database queries.

package main

import (
    "context"
    "database/sql"
    "fmt"
    "time"

    _ "github.com/lib/pq"
)

func fetch(ctx context.Context) {
    db, err := sql.Open("postgres", "host=localhost port=5432 user=postgres dbname=test sslmode=disable")
    if err != nil {
        fmt.Println("Error opening database:", err)
        return
    }
    defer db.Close()

    timeoutCtx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    rows, err := db.QueryContext(timeoutCtx, "SELECT * FROM users")
    if err != nil {
        fmt.Println("Database query failed:", err)
        return
    }
    defer rows.Close()

    for rows.Next() {
        fmt.Println("Processing row")
    }

    if err = rows.Err(); err != nil {
        fmt.Println("Error processing rows:", err)
        return
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    go fetch(ctx)
    time.Sleep(7 * time.Second)
}

In this example, a timeout context is created and used to query a database. If the query takes longer than 3 seconds, the timeout context is canceled, and the database query is stopped.

Example: Graceful Server Shutdown

Graceful shutdown is critical for server applications.

package main

import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func handler(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    select {
    case <-time.After(time.Second * 5):
        fmt.Fprintln(w, "Request processed successfully")
    case <-ctx.Done():
        fmt.Fprintln(w, "Request cancelled")
        fmt.Println(ctx.Err())
        return
    }
}

func main() {
    server := &http.Server{
        Addr:    ":8080",
        Handler: http.HandlerFunc(handler),
    }

    go func() {
        if err := server.ListenAndServe(); err != nil {
            fmt.Println("Server starting error:", err)
        }
    }()

    sig := make(chan os.Signal, 1)
    signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)

    fmt.Println("Server started, waiting for termination signal...")
    <-sig
    fmt.Println("Received termination signal, shutting down server...")

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        fmt.Println("Server shutdown error:", err)
    }

    fmt.Println("Server stopped")
}

In this example, the server listens for termination signals. When a termination signal is received, the server shuts down gracefully, ensuring that all goroutines are stopped within a specified timeout.

Example: Client-side Request Context Management

Contexts are also useful for managing client-side requests.

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

func fetch(ctx context.Context, url string) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
    client := http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("Request failed:", err)
        return
    }
    defer resp.Body.Close()
    fmt.Println("Request completed:", resp.Status)
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go fetch(ctx, "https://jsonplaceholder.typicode.com/todos/1")
    go fetch(ctx, "https://jsonplaceholder.typicode.com/todos/2")

    time.Sleep(5 * time.Second)
}

In this example, HTTP requests are made with a context that is canceled after 3 seconds.

Conclusion

The context package in Go is a powerful tool for managing goroutines efficiently. By using contexts effectively, you can handle cancellation, timeouts, and passing values between functions, leading to more robust and maintainable concurrent applications.