DEV Community

Cover image for Context in go, why it matters
Jnanesh D
Jnanesh D

Posted on

Context in go, why it matters

Understanding Context in Go: A Beginner's Guide

What is Context?

Context is Go's way of managing cancellations, timeouts, and request-scoped values across goroutines. Since Go is not an object-oriented language, you might find context the closest way of dealing with attributes attached to a particular flow of data.

πŸ“Š The Context Hierarchy

context.Background()  (root)
    β”‚
    β”œβ”€ WithTimeout ──→ Auto-cancels after duration
    β”‚
    β”œβ”€ WithCancel ──→ Manual cancellation
    β”‚
    β”œβ”€ WithDeadline ──→ Cancels at specific time
    β”‚
    └─ WithValue ──→ Carries request data
Enter fullscreen mode Exit fullscreen mode

Creating Contexts: Practical Examples

1️⃣ Background Context (Starting Point)

func main() {
    ctx := context.Background()
    processRequest(ctx)
}
Enter fullscreen mode Exit fullscreen mode

Output: Root context with no deadline or values


2️⃣ WithTimeout (Auto-Cancel)

package main

import (
    "context"
    "fmt"
    "math/rand"
    "time"
)

func SimpleRandom(donn chan bool) {
    // First we will take a random number
    randomNum := rand.Intn(100) > 50
    if !randomNum {
        time.Sleep(50 * time.Second)
    }
    time.Sleep(time.Second)
    donn <- true
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()
    chann := make(chan bool)
    go SimpleRandom(chann)

    select {
    case <-chann:
        fmt.Println("Ok we got the output within the deadline")

    case <-ctx.Done():
        fmt.Println("Timed out! No more waiting")
    }
}
Enter fullscreen mode Exit fullscreen mode

Let's say you are handling a CPU intensive task which takes random amount of time. Since we cannot wait indefinitely, we can use WithTimeout and wait for either channel or the context to timeout.

Output:

  • Ok we got the output within the deadline OR
  • Timed out! No more waiting

Use cases: OS operations, waiting for HTTP requests, and more.

Note: Always perform defer cancel() so that any memory leaks are avoided and cleanup happens carefully.


Use Case 2: Cancellable DB Query

Suppose we are connecting to a database in our code and the database is taking a lot of time. In that scenario also, context tells the connector to exit after a certain time.

func searchUsers(ctx context.Context, name string) ([]User, error) {
    query := "SELECT id, name, email FROM users WHERE name LIKE ?"

    rows, err := db.QueryContext(ctx, query, "%"+name+"%")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var users []User
    for rows.Next() {
        var u User
        if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
            return nil, err
        }
        users = append(users, u)
    }

    return users, nil
}

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

users, err := searchUsers(ctx, "John")
Enter fullscreen mode Exit fullscreen mode


Use Case 4: Request Pipeline with Tracing

When we are dealing with webserver handlers, we often find the same data being fetched in different places to achieve our goals. To solve this, we can use the WithValue of the context to store some variables.

In the above code, the Auth handler attaches the userId to the context and the subsequent pipeline executions can utilize the same instead of querying back.


Final Verdict

βœ… You can use context to manage the lifecycle of goroutines, prevent hangs, and enforce deadlines.

βœ… You can also use it to pass data to the chain of execution flows, primarily in middleware.

❌ Never store anything that is not request scoped in the context.


Happy coding! πŸš€

Top comments (0)