En los posts anteriores exploramos las goroutines, channels y el select statement. Hoy vamos a descubrir una de las características más importantes de Go para escribir código concurrente robusto: el context package. Si vienes de Java, context es similar a CompletableFuture.cancel() o ExecutorService.shutdownNow(), pero mucho más poderoso e integrado.
¿Qué es Context?
El context package proporciona una forma estándar de manejar:
✅ Cancelación: Cancelar operaciones en cascada
✅ Timeouts: Limitar el tiempo de ejecución
✅ Deadlines: Tiempos límite absolutos
✅ Valores: Pasar datos de request-scope (user IDs, trace IDs, etc.)
Comparación: Java vs Go
| Aspecto | Java | Go |
| Cancelación | Future.cancel(), ExecutorService.shutdown() | context.WithCancel() |
| Timeouts | Future.get(timeout, unit) | context.WithTimeout() |
| Propagación | Manual (pasando flags) | Automática (pasando context) |
| Valores | ThreadLocal (problemático) | Context values (seguro) |
| Integración | Librerías externas | Estándar del lenguaje |
Context Básico: Background y TODO
Go proporciona dos context base:
ctx := context.Background()
ctx := context.TODO()
Cuándo usar cada uno:
Background(): Context principal de tu aplicación (main, handlers HTTP, etc.)
TODO(): Cuando no estás seguro qué context usar (útil durante desarrollo)
Context con Timeout
Uno de los casos de uso más comunes es limitar el tiempo de ejecución de una operación:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
done := make(chan bool)
go func() {
time.Sleep(3 * time.Second)
done <- true
}()
select {
case <-done:
fmt.Println("Operation completed")
case <-ctx.Done():
fmt.Printf("Operation cancelled: %v\n", ctx.Err())
}
Características importantes:
✅ cancel() debe llamarse siempre para liberar recursos
✅ Usa defer cancel() para asegurar que se llame
✅ ctx.Done() retorna un channel que se cierra cuando el context se cancela
✅ ctx.Err() retorna el error (context.DeadlineExceeded o context.Canceled)
Verificar si un Context está Cancelado
if ctx.Err() != nil {
return ctx.Err()
}
Context con Cancelación Manual
A veces necesitas cancelar una operación manualmente, no por timeout:
ctx, cancel := context.WithCancel(context.Background())
go func() {
for i := 0; i < 10; i++ {
select {
case <-ctx.Done():
fmt.Printf("Worker cancelled: %v\n", ctx.Err())
return
default:
fmt.Printf("Working... %d\n", i)
time.Sleep(200 * time.Millisecond)
}
}
}()
time.Sleep(1 * time.Second)
fmt.Println("Cancelling context...")
cancel()
time.Sleep(500 * time.Millisecond)
Casos de uso:
✅ Cancelar cuando el usuario presiona "Cancelar"
✅ Cancelar cuando ocurre un error y no necesitas continuar
✅ Cancelar operaciones en cascada cuando una falla
Context con Deadline
Similar a timeout, pero con un tiempo absoluto:
deadline := time.Now().Add(2 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
select {
case <-time.After(3 * time.Second):
fmt.Println("Operation completed")
case <-ctx.Done():
fmt.Printf("Deadline exceeded: %v\n", ctx.Err())
}
Diferencia entre Timeout y Deadline:
Cuándo usar cada uno:
Context con Valores
Los context pueden llevar valores que se propagan a través de la cadena de llamadas. Esto es útil para request IDs, user IDs, trace IDs, etc.
ctx := context.WithValue(context.Background(), "userID", 123)
ctx = context.WithValue(ctx, "requestID", "req-456")
ctx = context.WithValue(ctx, "traceID", "trace-789")
processRequest := func(ctx context.Context) {
userID := ctx.Value("userID").(int)
requestID := ctx.Value("requestID").(string)
traceID := ctx.Value("traceID").(string)
fmt.Printf("Processing request:\n")
fmt.Printf(" User ID: %d\n", userID)
fmt.Printf(" Request ID: %s\n", requestID)
fmt.Printf(" Trace ID: %s\n", traceID)
}
processRequest(ctx)
⚠️ Mejores Prácticas para Valores
1. Usa tipos específicos para las keys:
type contextKey string
const (
userIDKey contextKey = "userID"
requestIDKey contextKey = "requestID"
)
ctx := context.WithValue(context.Background(), userIDKey, 123)
userID := ctx.Value(userIDKey).(int)
ctx := context.WithValue(context.Background(), "userID", 123)
2. Solo usa valores para datos de request-scope:
ctx = context.WithValue(ctx, "userID", userID)
ctx = context.WithValue(ctx, "requestID", requestID)
func processUser(ctx context.Context, userID int) {
}
3. Los valores deben ser inmutables:
ctx = context.WithValue(ctx, "userID", 123)
ctx = context.WithValue(ctx, "requestID", "req-123")
type Config struct {
APIKey string
}
ctx = context.WithValue(ctx, "config", &Config{APIKey: "secret"})
Propagando Context en Funciones
Una de las reglas más importantes en Go: siempre pasa context.Context como primer parámetro en funciones que pueden cancelarse o necesitan valores del context.
Convención de Nombres
func longRunningOperation(ctx context.Context, duration time.Duration) error {
}
func fetchData(ctx context.Context, url string) ([]byte, error) {
}
func processUser(ctx context.Context, userID int) error {
}
Ejemplo: Operación con Checks Periódicos
func longRunningOperation(ctx context.Context, duration time.Duration) error {
fmt.Printf("Starting operation (will take %v)...\n", duration)
if ctx.Err() != nil {
return ctx.Err()
}
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
start := time.Now()
for {
select {
case <-ctx.Done():
fmt.Printf("Operation cancelled after %v\n", time.Since(start))
return ctx.Err()
case <-ticker.C:
elapsed := time.Since(start)
fmt.Printf(" Still working... (%v elapsed)\n", elapsed)
if elapsed >= duration {
fmt.Printf("Operation completed in %v\n", elapsed)
return nil
}
}
}
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := longRunningOperation(ctx, 3*time.Second); err != nil {
fmt.Printf("Error: %v\n", err)
}
Puntos clave:
✅ Verifica ctx.Done() periódicamente en loops largos
✅ Retorna ctx.Err() cuando detectas cancelación
✅ Usa select para no bloquear innecesariamente
Context en HTTP Requests
El paquete net/http integra context directamente. Cada request tiene un context que puedes usar:
type HTTPClient struct{}
func (c *HTTPClient) Get(ctx context.Context, url string) (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(1 * time.Second):
return fmt.Sprintf("Response from %s", url), nil
}
}
client := &HTTPClient{}
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()
result, err := client.Get(ctx, "https://api.example.com/data")
if err != nil {
fmt.Printf("Request failed: %v\n", err)
} else {
fmt.Printf("Success: %s\n", result)
}
Context en HTTP Handlers
En handlers HTTP, el context viene del request:
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = context.WithValue(ctx, "userID", getUserID(r))
if err := processRequest(ctx); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Write([]byte("Success"))
}
Características del HTTP Context:
✅ Se cancela automáticamente cuando el cliente cierra la conexión
✅ Puedes agregar valores (user ID, request ID, etc.)
✅ Puedes crear timeouts específicos para operaciones dentro del handler
Context en Operaciones de Base de Datos
La mayoría de drivers de base de datos en Go aceptan context:
type Database struct{}
func (db *Database) Query(ctx context.Context, query string) ([]string, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(2 * time.Second):
return []string{"result1", "result2", "result3"}, nil
}
}
db := &Database{}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
results, err := db.Query(ctx, "SELECT * FROM users")
if err != nil {
fmt.Printf("Query failed: %v\n", err)
} else {
fmt.Printf("Results: %v\n", results)
}
Ejemplo Real con database/sql
import (
"context"
"database/sql"
"time"
)
func getUser(ctx context.Context, db *sql.DB, userID int) (*User, error) {
queryCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
var user User
err := db.QueryRowContext(queryCtx,
"SELECT id, name, email FROM users WHERE id = $1", userID).
Scan(&user.ID, &user.Name, &user.Email)
if err != nil {
return nil, err
}
return &user, nil
}
Context Chaining: Encadenar Contexts
Puedes encadenar contexts para agregar más información o timeouts:
baseCtx := context.WithValue(context.Background(), "userID", 123)
timeoutCtx, cancel1 := context.WithTimeout(baseCtx, 2*time.Second)
defer cancel1()
finalCtx := context.WithValue(timeoutCtx, "requestID", "req-999")
fmt.Printf("User ID: %d\n", finalCtx.Value("userID"))
fmt.Printf("Request ID: %s\n", finalCtx.Value("requestID"))
select {
case <-time.After(3 * time.Second):
fmt.Println("Operation completed")
case <-finalCtx.Done():
fmt.Printf("Timeout: %v\n", finalCtx.Err())
}
Características del chaining:
✅ Los valores se propagan hacia abajo
✅ Los timeouts/deadlines se heredan (el más corto gana)
✅ La cancelación se propaga hacia abajo
Ejemplo Práctico: Servicio con Múltiples Operaciones
Un ejemplo completo de cómo usar context en un servicio real:
type Service struct {
db *Database
client *HTTPClient
}
func NewService() *Service {
return &Service{
db: &Database{},
client: &HTTPClient{},
}
}
func (s *Service) ProcessData(ctx context.Context, userID int) error {
ctx = context.WithValue(ctx, "userID", userID)
results, err := s.db.Query(ctx, "SELECT * FROM data")
if err != nil {
return fmt.Errorf("database query failed: %w", err)
}
externalData, err := s.client.Get(ctx, "https://api.example.com/data")
if err != nil {
return fmt.Errorf("external API call failed: %w", err)
}
fmt.Printf("Processed data: %v, External: %s\n", results, externalData)
return nil
}
service := NewService()
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := service.ProcessData(ctx, 123); err != nil {
fmt.Printf("Error: %v\n", err)
}
Ventajas:
✅ Si la query de DB falla, el context se cancela y la llamada HTTP también puede cancelarse
✅ Si el timeout se alcanza, ambas operaciones se cancelan
✅ Los valores (userID) están disponibles en todas las funciones
Context en Worker Pools
Usar context para cancelar workers cuando sea necesario:
func workerPoolWithContext(ctx context.Context, numWorkers int) {
jobs := make(chan Task, 10)
results := make(chan Result, 10)
var wg sync.WaitGroup
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go func(workerID int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: Cancelled\n", workerID)
return
case task, ok := <-jobs:
if !ok {
return
}
result := processTask(ctx, task)
select {
case results <- result:
case <-ctx.Done():
return
}
}
}
}(w)
}
go func() {
defer close(jobs)
for i := 1; i <= 100; i++ {
select {
case jobs <- Task{ID: i}:
case <-ctx.Done():
return
}
}
}()
go func() {
wg.Wait()
close(results)
}()
for result := range results {
fmt.Printf("Result: %v\n", result)
}
}
Mejores Prácticas
1. Siempre Pasa Context como Primer Parámetro
func processData(ctx context.Context, data []byte) error {
}
func processData(data []byte, ctx context.Context) error {
}
2. Siempre Llama cancel() con defer
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
3. No Almacenes Context en Structs
type Service struct {
ctx context.Context
}
type Service struct {
db *Database
}
func (s *Service) Process(ctx context.Context, data Data) error {
}
Razón: Los context son específicos de request y no deben compartirse entre requests.
4. Usa Tipos Específicos para Context Keys
type contextKey string
const userIDKey contextKey = "userID"
ctx := context.WithValue(ctx, userIDKey, 123)
userID := ctx.Value(userIDKey).(int)
ctx := context.WithValue(ctx, "userID", 123)
5. Verifica Cancelación en Loops Largos
for i := 0; i < 1000000; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
}
for i := 0; i < 1000000; i++ {
}
6. No Pases nil Context
func process(ctx context.Context) {
if ctx == nil {
ctx = context.Background()
}
}
func process(ctx context.Context) {
if ctx == nil {
ctx = context.Background()
}
}
Errores Comunes
❌ Error 1: Olvidar Llamar cancel()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
❌ Error 2: Almacenar Context en Structs
type Service struct {
ctx context.Context
}
func NewService(ctx context.Context) *Service {
return &Service{ctx: ctx}
}
type Service struct {
db *Database
}
func (s *Service) Process(ctx context.Context, data Data) error {
}
❌ Error 3: No Verificar Cancelación en Loops
func processLargeDataset(ctx context.Context, data []Item) {
for _, item := range data {
processItem(item)
}
}
func processLargeDataset(ctx context.Context, data []Item) error {
for i, item := range data {
select {
case <-ctx.Done():
return ctx.Err()
default:
processItem(item)
}
if i%1000 == 0 {
if ctx.Err() != nil {
return ctx.Err()
}
}
}
return nil
}
❌ Error 4: Usar Context Values para Parámetros de Función
func getUser(ctx context.Context) (*User, error) {
userID := ctx.Value("userID").(int)
}
func getUser(ctx context.Context, userID int) (*User, error) {
}
Regla: Context values son para datos de request-scope (trace IDs, request IDs), no para parámetros de función.
❌ Error 5: Crear Context sin Background o TODO
var ctx context.Context
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
Comparación: Java vs Go
Java: Cancelación Manual
ExecutorService executor = Executors.newFixedThreadPool(10);
Future<String> future = executor.submit(() -> {
return "result";
});
try {
String result = future.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
future.cancel(true);
}
Go: Context
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := longOperation(ctx)
if err != nil {
}
Context y Select: Combinación Poderosa
El context y el select trabajan perfectamente juntos:
func processWithMultipleSources(ctx context.Context) error {
ch1 := make(chan Result)
ch2 := make(chan Result)
go fetchFromSource1(ctx, ch1)
go fetchFromSource2(ctx, ch2)
select {
case result := <-ch1:
return processResult(result)
case result := <-ch2:
return processResult(result)
case <-ctx.Done():
return ctx.Err()
}
}
Conclusiones
El context package es esencial para escribir código concurrente robusto en Go:
✅ Cancelación en cascada: Cancela operaciones hijas automáticamente
✅ Timeouts y deadlines: Limita el tiempo de ejecución de forma elegante
✅ Valores de request-scope: Pasa datos de forma segura sin ThreadLocal
✅ Integración estándar: Funciona con HTTP, databases, y todas las librerías estándar
✅ Propagación automática: Pasa el context y todo funciona
✅ Type-safe: Previene errores en tiempo de compilación
Si vienes de Java, el context puede parecer diferente al principio, pero una vez que entiendas cómo funciona y lo uses consistentemente, verás cómo simplifica enormemente el manejo de cancelación, timeouts y valores de request-scope en aplicaciones concurrentes.
Próximos Pasos
En el siguiente post exploraremos los patrones de sincronización en Go usando el paquete sync, incluyendo WaitGroup, Mutex, RWMutex, y otros primitivos de sincronización que complementan perfectamente las goroutines, channels y context.
¿Has usado context en tus proyectos de Go? ¿Qué patrones te han resultado más útiles? Comparte tus experiencias y casos de uso en los comentarios. Y si quieres ver el código completo de estos ejemplos, puedes encontrarlo en mi repositorio go-mastery-lab.