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:
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
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:
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:
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:
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
-
Always pass context as the first parameter in functions that perform I/O operations:
func fetchUserData(ctx context.Context, userID string) (*User, error) {
// Use the context for timeouts, cancellation, etc.
} -
Don't store context in struct fields - pass it explicitly as a parameter:
// 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
} -
Use context for cancellation, not for flow control:
// 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
}
} -
Be cautious with context.Value(). Use it primarily for request-scoped values, not for passing function parameters.
-
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:
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
- Context package documentation
- Go Blog: Go Concurrency Patterns: Context
- Gin Documentation on Context
Exercises
-
Create a Gin endpoint that fetches data from two different services and returns an error if either takes more than 2 seconds.
-
Implement a middleware that adds user information to the context after verifying a JWT token.
-
Create a long-polling endpoint that uses context to detect when a client disconnects.
-
Build a service that performs batch processing but can be cancelled via the context if the user navigates away from the page.
-
Create an endpoint that propagates the context to multiple goroutines and ensures all of them stop if the context is cancelled.
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!