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:
gofunc 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:
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
} -
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
}
} -
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.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)