Skip to main content

Go Context

In Go, the context package provides a way to carry deadlines, cancellations, and request-scoped values across API boundaries and between processes. Understanding context is crucial for writing robust, efficient web services with Gin.

What is Context?

Context in Go is a built-in package that helps you manage the lifetime of operations, especially in concurrent programs. A Context is an interface that contains:

  • Deadline information (when the context should expire)
  • Cancellation signals
  • Request-scoped key-value pairs

Context is particularly important in web applications like those built with Gin, where you need to handle request timeouts, cancel operations, and pass request-specific data through your application.

Why Context is Important

  • Resource Management: Prevents resource leaks by signaling when operations should be abandoned
  • Timeout Control: Allows you to set time limits on operations
  • Cancellation Propagation: Enables cancellation signals to flow through your program
  • Request-Scoped Values: Carries request-specific data through your application
  • Clean API Design: Avoids parameter bloat in function signatures

Basic Context Usage

Let's start with the basic usage of context in Go:

go
package main

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

func main() {
// Create a background context as the root context
ctx := context.Background()

// Derive a context with cancellation capability
ctx, cancel := context.WithCancel(ctx)

// Don't forget to call cancel when you're done
defer cancel()

// Start a goroutine that uses the context
go func() {
for {
select {
case <-ctx.Done():
fmt.Println("Work cancelled, cleaning up...")
return
default:
fmt.Println("Doing work...")
time.Sleep(500 * time.Millisecond)
}
}
}()

// Let it work for a while
time.Sleep(2 * time.Second)

// Cancel the context - this will signal the goroutine to stop
fmt.Println("Cancelling work...")
cancel()

// Give the goroutine time to clean up
time.Sleep(1 * time.Second)
}

Output:

Doing work...
Doing work...
Doing work...
Doing work...
Cancelling work...
Work cancelled, cleaning up...

Context in Gin Framework

Gin has built-in context support through its own gin.Context type, which wraps the standard context.Context. In Gin handlers, you have access to both.

Accessing the Context in Gin

go
package main

import (
"github.com/gin-gonic/gin"
"net/http"
"time"
)

func main() {
r := gin.Default()

r.GET("/ping", func(c *gin.Context) {
// Get the underlying context.Context
ctx := c.Request.Context()

// You can use both c (gin.Context) and ctx (context.Context)

// Using gin.Context
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})

r.Run(":8080")
}

Context Timeout

One common use of context is for timeouts. Here's how to implement a timeout in a Gin handler:

go
package main

import (
"context"
"github.com/gin-gonic/gin"
"net/http"
"time"
)

func main() {
r := gin.Default()

r.GET("/slow-operation", func(c *gin.Context) {
// Create a context with a 2-second timeout
ctx, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second)
defer cancel()

// Channel to receive result
resultChan := make(chan string, 1)

// Run the slow operation in a separate goroutine
go func() {
result := performSlowOperation()
resultChan <- result
}()

// Wait for either the operation to complete or the context to timeout
select {
case result := <-resultChan:
c.JSON(http.StatusOK, gin.H{"result": result})
case <-ctx.Done():
c.JSON(http.StatusRequestTimeout, gin.H{"error": "operation timed out"})
}
})

r.Run(":8080")
}

func performSlowOperation() string {
// Simulate a slow operation
time.Sleep(3 * time.Second)
return "Operation completed"
}

When you call this endpoint, it will return a timeout error after 2 seconds, even though the operation takes 3 seconds.

Context with Value

Context allows you to store and retrieve request-scoped values:

go
package main

import (
"context"
"github.com/gin-gonic/gin"
"net/http"
)

// Define key types for type safety
type contextKey string

const userIDKey contextKey = "userID"

func authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// In a real app, you would extract this from JWT or session
userID := "user-123"

// Create a new context with the user ID
ctx := context.WithValue(c.Request.Context(), userIDKey, userID)

// Update the request with the new context
c.Request = c.Request.WithContext(ctx)

c.Next()
}
}

func main() {
r := gin.Default()

// Use the authentication middleware
r.Use(authMiddleware())

r.GET("/profile", func(c *gin.Context) {
// Get the user ID from the context
ctx := c.Request.Context()
userID, ok := ctx.Value(userIDKey).(string)

if !ok {
c.JSON(http.StatusInternalServerError, gin.H{"error": "userID not found in context"})
return
}

// Use the userID to fetch user data (simulated here)
userData := fetchUserData(userID)

c.JSON(http.StatusOK, userData)
})

r.Run(":8080")
}

func fetchUserData(userID string) gin.H {
// In a real app, you would query a database
return gin.H{
"id": userID,
"name": "John Doe",
"email": "[email protected]",
}
}

Context Cancellation in Gin

When a client disconnects, Gin automatically cancels the request context. You can listen for this cancellation:

go
func longRunningHandler(c *gin.Context) {
// Get the context from the request
ctx := c.Request.Context()

// Channel for the operation result
resultChan := make(chan gin.H, 1)

// Start the operation in a goroutine
go func() {
// Simulate a long-running operation
for i := 0; i < 10; i++ {
// Check for cancellation between steps
select {
case <-ctx.Done():
fmt.Println("Operation cancelled by client")
return
default:
// Continue with the operation
time.Sleep(500 * time.Millisecond)
fmt.Println("Processing step", i+1)
}
}

resultChan <- gin.H{"status": "completed"}
}()

// Wait for either completion or cancellation
select {
case result := <-resultChan:
c.JSON(http.StatusOK, result)
case <-ctx.Done():
// The client disconnected
c.AbortWithStatusJSON(http.StatusGone, gin.H{
"error": "client disconnected",
})
}
}

Best Practices for Working with Context

  1. Always pass context as the first parameter in functions that perform I/O operations:

    go
    func fetchUserData(ctx context.Context, userID string) (*User, error) {
    // Use the context for timeouts, cancellation, etc.
    }
  2. Don't store context in struct fields - pass it explicitly as a parameter:

    go
    // Incorrect
    type Service struct {
    ctx context.Context
    }

    // Correct
    type Service struct {
    // No context here
    }

    func (s *Service) DoSomething(ctx context.Context) error {
    // Use the context here
    }
  3. Use context for cancellation, not for flow control:

    go
    // Avoid this pattern
    if ctx.Value("admin").(bool) {
    // Admin-specific logic
    }

    // Better approach
    func handleRequest(ctx context.Context, user User) {
    if user.IsAdmin {
    // Admin-specific logic
    }
    }
  4. Be cautious with context.Value(). Use it primarily for request-scoped values, not for passing function parameters.

  5. Always call cancel functions that are returned from context creation to avoid resource leaks.

Real-world Example: Database Query with Timeout

Here's a complete example showing how to use context with a database query in Gin:

go
package main

import (
"context"
"database/sql"
"github.com/gin-gonic/gin"
_ "github.com/go-sql-driver/mysql"
"net/http"
"time"
)

type Product struct {
ID int `json:"id"`
Name string `json:"name"`
Price float64 `json:"price"`
}

func main() {
// Connect to database
db, err := sql.Open("mysql", "user:password@tcp(127.0.0.1:3306)/store")
if err != nil {
panic(err)
}
defer db.Close()

r := gin.Default()

r.GET("/products/:id", func(c *gin.Context) {
// Get product ID from URL parameter
idStr := c.Param("id")

// Create a context with a 3-second timeout
ctx, cancel := context.WithTimeout(c.Request.Context(), 3*time.Second)
defer cancel()

// Query the database with the context
product, err := getProduct(ctx, db, idStr)
if err != nil {
if err == context.DeadlineExceeded {
c.JSON(http.StatusGatewayTimeout, gin.H{"error": "database query timed out"})
} else if err == sql.ErrNoRows {
c.JSON(http.StatusNotFound, gin.H{"error": "product not found"})
} else {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
}
return
}

c.JSON(http.StatusOK, product)
})

r.Run(":8080")
}

func getProduct(ctx context.Context, db *sql.DB, id string) (*Product, error) {
var product Product

// Use QueryRowContext instead of QueryRow to respect the context
err := db.QueryRowContext(
ctx,
"SELECT id, name, price FROM products WHERE id = ?",
id,
).Scan(&product.ID, &product.Name, &product.Price)

if err != nil {
return nil, err
}

return &product, nil
}

Summary

Context in Go is a powerful mechanism for controlling concurrency and managing the lifecycle of operations. In Gin applications, it's essential for:

  • Setting timeouts for requests
  • Handling client disconnections gracefully
  • Carrying request-scoped values (like user IDs and authentication info)
  • Cancelling operations when they're no longer needed

By understanding and correctly using context, you can build more robust and responsive web applications with Go and Gin.

Further Resources

Exercises

  1. Create a Gin endpoint that fetches data from two different services and returns an error if either takes more than 2 seconds.

  2. Implement a middleware that adds user information to the context after verifying a JWT token.

  3. Create a long-polling endpoint that uses context to detect when a client disconnects.

  4. Build a service that performs batch processing but can be cancelled via the context if the user navigates away from the page.

  5. Create an endpoint that propagates the context to multiple goroutines and ensures all of them stop if the context is cancelled.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)