Go Concurrente: Goroutines, Channels y Context - De Cero a Experto

Go Concurrente: Goroutines, Channels y Context - De Cero a Experto

Guía exhaustiva sobre programación concurrente en Go 1.25: goroutines, channels, context, patrones, race conditions, manejo de errores, y arquitectura profesional. Desde principiante absoluto hasta nivel experto con ejemplos prácticos.

Por Omar Flores

Go Concurrente: Goroutines, Channels y Context

De Cero Absoluto a Arquitecto de Sistemas Concurrentes


🎯 Introducción: Por Qué Go Cambió las Reglas del Juego

Déjame ser directo: Go no es solo otro lenguaje de programación. Es una filosofía diferente sobre cómo construir software que escala.

Cuando Rob Pike, Ken Thompson y Robert Griesemer crearon Go en Google en 2007, tenían un problema específico: los sistemas de Google manejaban millones de conexiones simultáneas, y los lenguajes existentes hacían esto innecesariamente complicado.

El problema con otros lenguajes:
- Java: Threads pesados (1MB de stack por thread)
- Python: Global Interpreter Lock (GIL)
- C++: Complejidad brutal, memory management manual
- Node.js: Single-threaded, callback hell

El problema real:
- 10,000 conexiones simultáneas = 10GB de RAM solo en stacks
- Código de concurrencia difícil de escribir y mantener
- Bugs de race conditions casi imposibles de debugear

Go resolvió esto con tres innovaciones fundamentales:

  1. Goroutines: Hilos ultra-ligeros (2KB inicial vs 1MB de threads)
  2. Channels: Comunicación segura entre goroutines
  3. Context: Cancelación y timeouts propagados elegantemente

El resultado: Un servidor Go puede manejar 1 millón de goroutines en la misma memoria donde Java manejaría 1,000 threads.

Go 1.25: La Versión Más Madura

Esta guía está actualizada para Go 1.25.5 (Enero 2026), que incluye mejoras significativas:

VersiónMejoras Clave
Go 1.22Loop variable scoping, range-over-int
Go 1.23Iteradores (iter.Seq), unique package
Go 1.24Mejoras de rendimiento en scheduler
Go 1.25sync.OnceValues, optimizaciones de GC, structured concurrency patterns

Lo Que Esta Guía Cubre

Esta no es una referencia de sintaxis. Es una guía completa para dominar la concurrencia en Go:

Fundamentos de Go - Lo esencial antes de concurrencia
Goroutines - Qué son, cómo funcionan, cuándo usarlas
Channels - El corazón de la comunicación en Go
Select - Multiplexación de operaciones concurrentes
Context - Cancelación, timeouts, y propagación de valores
Patrones - Worker pools, fan-out/fan-in, pipelines
Race Conditions - Detectarlas, prevenirlas, debugearlas
Sync Package - Mutex, WaitGroup, Once, Pool
Go 1.23-1.25 - Iteradores, unique, nuevas APIs
Errores Comunes - Y cómo evitarlos
Arquitectura - Diseño de sistemas concurrentes reales

Prerequisitos

  • Conocimiento básico de programación (cualquier lenguaje)
  • Terminal/línea de comandos
  • Go 1.25+ instalado (usaremos features modernos)

Si nunca has usado Go, no te preocupes. Empezamos desde cero.


🏗️ Parte 1: Fundamentos de Go - Lo Esencial

Antes de hablar de concurrencia, necesitas entender Go. Esta sección es un curso acelerado de lo esencial.

Instalación y Setup (Go 1.25+)

# Linux/Mac
wget https://go.dev/dl/go1.25.5.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.25.5.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin

# Verificar instalación
go version
# go version go1.25.5 linux/amd64

Tu Primer Programa Go

Crea un archivo main.go:

package main

import "fmt"

func main() {
    fmt.Println("¡Hola, Go!")
}

Ejecutar:

go run main.go
# ¡Hola, Go!

Anatomía de un Programa Go

package main          // Todo archivo Go pertenece a un package
                      // 'main' es especial: es el punto de entrada

import "fmt"          // Importamos packages
                      // 'fmt' es para formateo e impresión

func main() {         // La función main() es donde empieza la ejecución
    // Tu código aquí
}

Variables y Tipos Básicos

package main

import "fmt"

func main() {
    // Declaración explícita
    var nombre string = "Carlos"
    var edad int = 30
    var activo bool = true

    // Declaración corta (inferencia de tipos) - MÁS COMÚN
    apellido := "García"      // Go infiere que es string
    altura := 1.75            // Go infiere que es float64

    // Múltiples variables
    x, y, z := 1, 2, 3

    // Constantes
    const PI = 3.14159
    const MaxConexiones = 100

    fmt.Printf("Nombre: %s %s\n", nombre, apellido)
    fmt.Printf("Edad: %d, Altura: %.2f\n", edad, altura)
    fmt.Printf("Activo: %t\n", activo)
}

Tipos básicos en Go:

// Enteros
int, int8, int16, int32, int64
uint, uint8, uint16, uint32, uint64

// Flotantes
float32, float64

// Otros
bool          // true o false
string        // cadenas de texto (inmutables)
byte          // alias de uint8
rune          // alias de int32 (para Unicode)

Funciones

package main

import "fmt"

// Función simple
func saludar(nombre string) {
    fmt.Printf("Hola, %s!\n", nombre)
}

// Función con retorno
func sumar(a, b int) int {
    return a + b
}

// Múltiples retornos (MUY COMÚN en Go)
func dividir(a, b float64) (float64, error) {
    if b == 0 {
        return 0, fmt.Errorf("división por cero")
    }
    return a / b, nil
}

// Retornos nombrados
func rectangulo(ancho, alto float64) (area, perimetro float64) {
    area = ancho * alto
    perimetro = 2 * (ancho + alto)
    return  // return "desnudo" - retorna las variables nombradas
}

func main() {
    saludar("María")

    resultado := sumar(5, 3)
    fmt.Println("5 + 3 =", resultado)

    // Manejo de errores (patrón fundamental en Go)
    cociente, err := dividir(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("10 / 2 =", cociente)

    a, p := rectangulo(5, 3)
    fmt.Printf("Área: %.2f, Perímetro: %.2f\n", a, p)
}

El Patrón de Manejo de Errores en Go

Go no tiene excepciones. Usa retornos múltiples con errores explícitos:

// ❌ NO existe en Go
try {
    resultado = operacionRiesgosa()
} catch (error) {
    // manejar error
}

// ✅ Así se hace en Go
resultado, err := operacionRiesgosa()
if err != nil {
    // manejar error
    return err  // o log, o recuperar
}
// usar resultado

Este patrón te obliga a pensar en los errores en cada paso. Es verbose, pero te salva de bugs silenciosos.

Slices y Maps

package main

import "fmt"

func main() {
    // SLICES (arreglos dinámicos)
    // Crear un slice vacío
    var numeros []int
    numeros = append(numeros, 1, 2, 3)

    // Crear con valores iniciales
    frutas := []string{"manzana", "banana", "cereza"}

    // Crear con capacidad (más eficiente)
    datos := make([]int, 0, 100)  // len=0, cap=100

    // Acceso y modificación
    fmt.Println(frutas[0])      // "manzana"
    frutas[1] = "blueberry"     // modificar

    // Slicing
    primerosDos := frutas[0:2]  // ["manzana", "blueberry"]

    // Iterar
    for i, fruta := range frutas {
        fmt.Printf("%d: %s\n", i, fruta)
    }

    // MAPS (diccionarios)
    // Crear un map
    edades := make(map[string]int)
    edades["Carlos"] = 30
    edades["María"] = 25

    // Crear con valores iniciales
    capitales := map[string]string{
        "México":    "CDMX",
        "Argentina": "Buenos Aires",
        "España":    "Madrid",
    }

    // Acceso
    edad := edades["Carlos"]
    fmt.Println("Carlos tiene", edad, "años")

    // Verificar si existe
    capital, existe := capitales["Chile"]
    if !existe {
        fmt.Println("Chile no está en el mapa")
    }

    // Eliminar
    delete(edades, "Carlos")

    // Iterar
    for pais, capital := range capitales {
        fmt.Printf("%s -> %s\n", pais, capital)
    }
}

Structs (Estructuras)

package main

import "fmt"

// Definir una estructura
type Persona struct {
    Nombre    string
    Edad      int
    Email     string
    Activo    bool
}

// Método asociado a Persona
func (p Persona) Saludar() string {
    return fmt.Sprintf("Hola, soy %s y tengo %d años", p.Nombre, p.Edad)
}

// Método que modifica (necesita puntero)
func (p *Persona) CumplirAnios() {
    p.Edad++
}

func main() {
    // Crear una instancia
    carlos := Persona{
        Nombre: "Carlos",
        Edad:   30,
        Email:  "carlos@email.com",
        Activo: true,
    }

    // Acceder a campos
    fmt.Println(carlos.Nombre)

    // Llamar métodos
    fmt.Println(carlos.Saludar())

    carlos.CumplirAnios()
    fmt.Println("Después del cumpleaños:", carlos.Edad)

    // Punteros
    ptr := &carlos  // puntero a carlos
    ptr.Nombre = "Carlos García"  // Go auto-dereferencia
    fmt.Println(carlos.Nombre)  // "Carlos García"
}

Interfaces

Las interfaces en Go son implícitas. No declaras que un tipo implementa una interface; simplemente lo hace si tiene los métodos correctos.

package main

import (
    "fmt"
    "math"
)

// Definir una interface
type Figura interface {
    Area() float64
    Perimetro() float64
}

// Rectángulo implementa Figura (implícitamente)
type Rectangulo struct {
    Ancho, Alto float64
}

func (r Rectangulo) Area() float64 {
    return r.Ancho * r.Alto
}

func (r Rectangulo) Perimetro() float64 {
    return 2 * (r.Ancho + r.Alto)
}

// Círculo también implementa Figura
type Circulo struct {
    Radio float64
}

func (c Circulo) Area() float64 {
    return math.Pi * c.Radio * c.Radio
}

func (c Circulo) Perimetro() float64 {
    return 2 * math.Pi * c.Radio
}

// Función que acepta cualquier Figura
func imprimirInfo(f Figura) {
    fmt.Printf("Área: %.2f, Perímetro: %.2f\n", f.Area(), f.Perimetro())
}

func main() {
    rect := Rectangulo{Ancho: 5, Alto: 3}
    circ := Circulo{Radio: 2.5}

    // Ambos pueden pasarse a imprimirInfo
    imprimirInfo(rect)
    imprimirInfo(circ)

    // Slice de interfaces
    figuras := []Figura{rect, circ}
    for _, f := range figuras {
        imprimirInfo(f)
    }
}

La Interface Vacía: any (antes interface{})

package main

import "fmt"

func imprimir(valor any) {
    fmt.Printf("Tipo: %T, Valor: %v\n", valor, valor)
}

func main() {
    imprimir(42)
    imprimir("hola")
    imprimir(3.14)
    imprimir(true)

    // Type assertion
    var x any = "texto"

    // Forma insegura (panic si falla)
    s := x.(string)
    fmt.Println(s)

    // Forma segura
    if s, ok := x.(string); ok {
        fmt.Println("Es string:", s)
    }

    // Type switch
    switch v := x.(type) {
    case string:
        fmt.Println("Es string:", v)
    case int:
        fmt.Println("Es int:", v)
    default:
        fmt.Println("Tipo desconocido")
    }
}

📊 Resumen de la Parte 1

Ahora conoces los fundamentos de Go:

ConceptoGoNotas
Variablesx := valorInferencia de tipos
Funcionesfunc nombre(params) retornoMúltiples retornos
Erroresresultado, err := fn()Sin excepciones
Slices[]tipoArreglos dinámicos
Mapsmap[key]valueDiccionarios
Structstype Nombre struct{}Con métodos
InterfacesImplícitasDuck typing
flowchart TD
    subgraph Fundamentos["Fundamentos de Go"]
        A[Variables y Tipos] --> B[Funciones]
        B --> C[Manejo de Errores]
        C --> D[Slices y Maps]
        D --> E[Structs]
        E --> F[Interfaces]
    end

    F --> G[🚀 Listo para Concurrencia]

    style G fill:#10b981,color:#fff

Con estos fundamentos, estás listo para el plato fuerte: Goroutines y Concurrencia.


🚀 Parte 2: Goroutines - Concurrencia Ligera

¿Qué es una Goroutine?

Una goroutine es una función que se ejecuta de forma concurrente con otras goroutines. Piensa en ella como un “hilo ultra-ligero” gestionado por el runtime de Go, no por el sistema operativo.

Analogía del Restaurante:

Imagina un restaurante:

MODELO TRADICIONAL (Threads del SO):
- Cada cliente que llega = 1 mesero dedicado
- 1000 clientes = 1000 meseros
- Cada mesero ocupa espacio, come, cobra sueldo
- El restaurante colapsa rápido

MODELO GO (Goroutines):
- 1000 clientes llegan
- Solo 4 meseros (= número de CPUs)
- Los meseros atienden tareas de todos los clientes
- Mientras un cliente piensa qué ordenar, el mesero atiende a otro
- El restaurante escala sin problemas

Comparación: Threads vs Goroutines

CaracterísticaThread del SOGoroutine
Memoria inicial~1MB~2KB
CreaciónCostosa (syscall)Barata (función)
Cambio contextoLento (kernel)Rápido (userspace)
Cantidad práctica~1,000s~1,000,000s
GestiónSistema operativoRuntime de Go

Tu Primera Goroutine

package main

import (
    "fmt"
    "time"
)

func saludar(nombre string) {
    for i := 0; i < 3; i++ {
        fmt.Printf("Hola %s! (iteración %d)\n", nombre, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // EJECUCIÓN SECUENCIAL (sin goroutines)
    fmt.Println("=== Secuencial ===")
    saludar("Carlos")
    saludar("María")

    // EJECUCIÓN CONCURRENTE (con goroutines)
    fmt.Println("\n=== Concurrente ===")
    go saludar("Carlos")  // 'go' lanza una goroutine
    go saludar("María")   // otra goroutine

    // ¡PROBLEMA! El programa termina antes de que las goroutines terminen
    time.Sleep(500 * time.Millisecond)  // hack temporal
    fmt.Println("Fin del programa")
}

Salida típica concurrente (orden no garantizado):

=== Secuencial ===
Hola Carlos! (iteración 0)
Hola Carlos! (iteración 1)
Hola Carlos! (iteración 2)
Hola María! (iteración 0)
Hola María! (iteración 1)
Hola María! (iteración 2)

=== Concurrente ===
Hola Carlos! (iteración 0)
Hola María! (iteración 0)
Hola María! (iteración 1)
Hola Carlos! (iteración 1)
Hola Carlos! (iteración 2)
Hola María! (iteración 2)
Fin del programa

El Problema del Main que Termina

package main

import "fmt"

func main() {
    go fmt.Println("Hola desde goroutine")
    // El programa termina aquí ANTES de que la goroutine ejecute
}

Resultado: Probablemente no imprime nada. El main() es una goroutine también, y cuando termina, el programa termina, matando todas las goroutines activas.

Solución Correcta: sync.WaitGroup

package main

import (
    "fmt"
    "sync"
    "time"
)

func trabajador(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // Notifica que terminó (SIEMPRE con defer)

    fmt.Printf("Trabajador %d: iniciando\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Trabajador %d: terminado\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)  // Incrementa el contador ANTES de lanzar
        go trabajador(i, &wg)  // Pasa por puntero
    }

    fmt.Println("Esperando a que terminen los trabajadores...")
    wg.Wait()  // Bloquea hasta que el contador llegue a 0
    fmt.Println("Todos los trabajadores terminaron")
}

Flujo del WaitGroup:

sequenceDiagram
    participant M as Main
    participant WG as WaitGroup
    participant W1 as Worker 1
    participant W2 as Worker 2

    M->>WG: Add(1)
    M->>W1: go trabajador(1)
    M->>WG: Add(1)
    M->>W2: go trabajador(2)
    M->>WG: Wait() [bloquea]

    W1->>W1: Trabaja...
    W2->>W2: Trabaja...

    W1->>WG: Done()
    W2->>WG: Done()

    WG->>M: Continúa (contador = 0)

Errores Comunes con WaitGroup

// ❌ ERROR 1: Add() dentro de la goroutine (race condition)
for i := 0; i < 5; i++ {
    go func() {
        wg.Add(1)  // ¡INCORRECTO! Puede que Wait() ya haya pasado
        defer wg.Done()
        // trabajo
    }()
}

// ✅ CORRECTO: Add() antes de lanzar
for i := 0; i < 5; i++ {
    wg.Add(1)
    go func() {
        defer wg.Done()
        // trabajo
    }()
}

// ❌ ERROR 2: Pasar WaitGroup por valor
go trabajador(i, wg)  // Copia el WaitGroup - Done() no afecta al original

// ✅ CORRECTO: Pasar por puntero
go trabajador(i, &wg)

// ❌ ERROR 3: Olvidar Done()
go func() {
    // trabajo
    // wg.Done() olvidado - Wait() bloquea para siempre
}()

// ✅ CORRECTO: Usar defer
go func() {
    defer wg.Done()  // SIEMPRE se ejecuta, incluso con panic
    // trabajo
}()

Goroutines Anónimas y Closures

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup

    // ❌ ERROR CLÁSICO: Closure captura variable del loop
    fmt.Println("=== Incorrecto ===")
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Println(i)  // Probablemente imprime 3, 3, 3
        }()
    }
    wg.Wait()

    // ✅ CORRECTO: Pasar como parámetro
    fmt.Println("\n=== Correcto (parámetro) ===")
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func(n int) {  // n es una copia de i
            defer wg.Done()
            fmt.Println(n)
        }(i)  // Pasa i como argumento
    }
    wg.Wait()

    // ✅ CORRECTO en Go 1.22+: Loop variable scoping
    fmt.Println("\n=== Correcto (Go 1.22+) ===")
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            fmt.Println(i)  // Go 1.22+ captura correctamente
        }()
    }
    wg.Wait()
}

Nota Go 1.22+: A partir de Go 1.22, las variables de loop tienen scope por iteración, eliminando este bug clásico. En Go 1.25 esto es comportamiento estándar y ya no necesitas workarounds.

¿Cuántas Goroutines Puedo Lanzar?

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func main() {
    // Número de CPUs disponibles
    fmt.Println("CPUs:", runtime.NumCPU())

    // Crear MUCHAS goroutines
    var wg sync.WaitGroup
    numGoroutines := 100_000

    start := time.Now()

    for i := 0; i < numGoroutines; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // Simular algo de trabajo
            time.Sleep(10 * time.Millisecond)
        }(i)
    }

    // ¿Cuántas goroutines hay activas?
    fmt.Printf("Goroutines activas: %d\n", runtime.NumGoroutine())

    wg.Wait()
    fmt.Printf("Tiempo total: %v\n", time.Since(start))
}

Resultado típico:

CPUs: 8
Goroutines activas: 100001
Tiempo total: ~50ms  (no 1000 segundos)

100,000 goroutines ejecutadas en paralelo con 8 CPUs en ~50ms. Eso es el poder de Go.

Cómo Funciona Internamente: El Scheduler de Go

Go usa un modelo M:N (M goroutines sobre N threads del SO):

flowchart TB
    subgraph Goroutines["Goroutines (miles/millones)"]
        G1[G1]
        G2[G2]
        G3[G3]
        G4[G4]
        G5[G5]
        G6[G6]
    end

    subgraph Processors["Procesadores Lógicos (P)"]
        P1[P1]
        P2[P2]
    end

    subgraph Threads["Threads del SO (M)"]
        M1[M1]
        M2[M2]
        M3[M3]
    end

    subgraph CPU["CPUs Físicos"]
        C1[Core 1]
        C2[Core 2]
    end

    G1 --> P1
    G2 --> P1
    G3 --> P1
    G4 --> P2
    G5 --> P2
    G6 --> P2

    P1 --> M1
    P2 --> M2

    M1 --> C1
    M2 --> C2

Conceptos clave:

  • G (Goroutine): Tu código concurrente
  • P (Processor): Cola de goroutines listas para ejecutar
  • M (Machine): Thread del sistema operativo

El scheduler de Go:

  1. Asigna goroutines a Ps
  2. Cada P se ejecuta en un M
  3. Cuando una goroutine bloquea (I/O, syscall), el scheduler mueve otras goroutines a otro M
  4. Work stealing: Si un P no tiene trabajo, roba goroutines de otro P

GOMAXPROCS: Controlando el Paralelismo

package main

import (
    "fmt"
    "runtime"
)

func main() {
    // Ver el valor actual
    fmt.Println("GOMAXPROCS actual:", runtime.GOMAXPROCS(0))

    // Establecer un nuevo valor (retorna el anterior)
    anterior := runtime.GOMAXPROCS(4)
    fmt.Println("GOMAXPROCS anterior:", anterior)
    fmt.Println("GOMAXPROCS nuevo:", runtime.GOMAXPROCS(0))

    // Por defecto, GOMAXPROCS = número de CPUs
    runtime.GOMAXPROCS(runtime.NumCPU())
}

¿Cuándo modificar GOMAXPROCS?

  • Casi nunca. El valor por defecto es óptimo para la mayoría de casos.
  • En containers: Usa runtime/cgroup para detectar límites correctamente
  • Para testing: GOMAXPROCS=1 serializa goroutines (útil para debugging)

Ejemplo Práctico: Procesamiento de Imágenes Concurrente

package main

import (
    "fmt"
    "sync"
    "time"
)

type Imagen struct {
    ID       int
    Filename string
}

func procesarImagen(img Imagen) {
    // Simula procesamiento (resize, filtros, etc.)
    time.Sleep(100 * time.Millisecond)
    fmt.Printf("Imagen %d (%s) procesada\n", img.ID, img.Filename)
}

func main() {
    imagenes := []Imagen{
        {1, "foto1.jpg"},
        {2, "foto2.jpg"},
        {3, "foto3.jpg"},
        {4, "foto4.jpg"},
        {5, "foto5.jpg"},
    }

    // SECUENCIAL
    fmt.Println("=== Procesamiento Secuencial ===")
    start := time.Now()
    for _, img := range imagenes {
        procesarImagen(img)
    }
    fmt.Printf("Tiempo secuencial: %v\n\n", time.Since(start))

    // CONCURRENTE
    fmt.Println("=== Procesamiento Concurrente ===")
    start = time.Now()
    var wg sync.WaitGroup
    for _, img := range imagenes {
        wg.Add(1)
        go func(img Imagen) {
            defer wg.Done()
            procesarImagen(img)
        }(img)
    }
    wg.Wait()
    fmt.Printf("Tiempo concurrente: %v\n", time.Since(start))
}

Salida típica:

=== Procesamiento Secuencial ===
Imagen 1 (foto1.jpg) procesada
Imagen 2 (foto2.jpg) procesada
Imagen 3 (foto3.jpg) procesada
Imagen 4 (foto4.jpg) procesada
Imagen 5 (foto5.jpg) procesada
Tiempo secuencial: 500ms

=== Procesamiento Concurrente ===
Imagen 3 (foto3.jpg) procesada
Imagen 1 (foto1.jpg) procesada
Imagen 5 (foto5.jpg) procesada
Imagen 2 (foto2.jpg) procesada
Imagen 4 (foto4.jpg) procesada
Tiempo concurrente: 100ms

¡5x más rápido con concurrencia!


📊 Resumen de la Parte 2

ConceptoSintaxisUso
Lanzar goroutinego funcion()Ejecutar concurrentemente
Esperar goroutinessync.WaitGroupSincronización
Goroutine anónimago func() { }()Código inline
CPUs disponiblesruntime.NumCPU()Información del sistema
Paralelismoruntime.GOMAXPROCS(n)Controlar threads
flowchart LR
    subgraph Goroutines["Lo que aprendiste"]
        A[go keyword] --> B[WaitGroup]
        B --> C[Closures]
        C --> D[Scheduler]
        D --> E[GOMAXPROCS]
    end

    E --> F[🔌 Listo para Channels]

    style F fill:#3b82f6,color:#fff

Las goroutines son poderosas, pero tienen un problema: ¿cómo se comunican entre sí?

La respuesta: Channels.


🔌 Parte 3: Channels - Comunicación Entre Goroutines

El Mantra de Go

“No comuniques compartiendo memoria; comparte memoria comunicando.”

— Rob Pike, co-creador de Go

Esta frase define la filosofía de Go. En lugar de tener datos compartidos protegidos con locks (como en Java/C++), Go prefiere que las goroutines se envíen datos unas a otras a través de channels.

¿Qué es un Channel?

Un channel es un conducto tipado a través del cual puedes enviar y recibir valores. Es como una tubería: un lado mete datos, el otro lado los saca.

flowchart LR
    G1[Goroutine 1] -->|envía dato| CH((Channel))
    CH -->|recibe dato| G2[Goroutine 2]

    style CH fill:#3b82f6,color:#fff

Analogía de la Panadería:

Imagina una panadería con un mostrador:

- El panadero (goroutine 1) hornea pan y lo pone en el mostrador
- El cliente (goroutine 2) toma pan del mostrador
- El mostrador ES el channel

Reglas del mostrador:
- Si no hay pan, el cliente espera
- Si el mostrador está lleno, el panadero espera
- No hay conflictos: uno pone, otro toma

Crear y Usar Channels

package main

import "fmt"

func main() {
    // Crear un channel de enteros
    ch := make(chan int)

    // Enviar en una goroutine (porque el channel sin buffer bloquea)
    go func() {
        ch <- 42  // Enviar valor al channel
    }()

    // Recibir del channel
    valor := <-ch  // Recibir valor del channel
    fmt.Println("Recibido:", valor)
}

Sintaxis de Channels

// Crear channels
ch := make(chan int)           // channel sin buffer (unbuffered)
ch := make(chan string, 10)    // channel con buffer de 10 elementos

// Operaciones
ch <- valor    // Enviar al channel (bloquea si está lleno)
valor := <-ch  // Recibir del channel (bloquea si está vacío)
valor, ok := <-ch  // ok=false si channel cerrado y vacío

// Cerrar channel (solo el sender debería cerrar)
close(ch)

// Tipos direccionales
func enviar(ch chan<- int) {}   // Solo puede enviar
func recibir(ch <-chan int) {}  // Solo puede recibir

Channels Sin Buffer (Unbuffered)

Un channel sin buffer sincroniza las goroutines: el envío bloquea hasta que alguien reciba, y viceversa.

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)  // Sin buffer

    go func() {
        fmt.Println("Goroutine: preparando mensaje...")
        time.Sleep(2 * time.Second)
        ch <- "¡Hola desde la goroutine!"  // BLOQUEA hasta que main reciba
        fmt.Println("Goroutine: mensaje enviado")
    }()

    fmt.Println("Main: esperando mensaje...")
    msg := <-ch  // BLOQUEA hasta que la goroutine envíe
    fmt.Println("Main: recibido:", msg)
}

Salida:

Main: esperando mensaje...
Goroutine: preparando mensaje...
Main: recibido: ¡Hola desde la goroutine!
Goroutine: mensaje enviado
sequenceDiagram
    participant M as Main
    participant CH as Channel
    participant G as Goroutine

    G->>G: Preparando (2s)
    M->>CH: <-ch [BLOQUEA]
    G->>CH: ch <- msg
    CH->>M: msg recibido
    Note over M,G: Ambos continúan

Channels Con Buffer

Un channel con buffer puede almacenar N elementos. Solo bloquea cuando está lleno (al enviar) o vacío (al recibir).

package main

import "fmt"

func main() {
    // Buffer de 3 elementos
    ch := make(chan int, 3)

    // Podemos enviar 3 veces sin bloquear
    ch <- 1
    ch <- 2
    ch <- 3
    // ch <- 4  // ¡Esto bloquearía! (buffer lleno, nadie recibe)

    fmt.Println(<-ch)  // 1
    fmt.Println(<-ch)  // 2
    fmt.Println(<-ch)  // 3
}

¿Cuándo usar buffer?

Sin BufferCon Buffer
Sincronización fuerteDesacoplar producer/consumer
Handoff directoAbsorber ráfagas de trabajo
Más simple de razonarMás throughput

Cerrar Channels

package main

import "fmt"

func main() {
    ch := make(chan int, 3)

    // Enviar algunos valores
    ch <- 1
    ch <- 2
    ch <- 3

    close(ch)  // Cerrar el channel

    // Recibir de channel cerrado
    fmt.Println(<-ch)  // 1
    fmt.Println(<-ch)  // 2
    fmt.Println(<-ch)  // 3
    fmt.Println(<-ch)  // 0 (valor zero del tipo)

    // Detectar si está cerrado
    val, ok := <-ch
    fmt.Printf("Valor: %d, Abierto: %t\n", val, ok)  // Valor: 0, Abierto: false
}

Reglas importantes de close():

// ✅ El SENDER cierra el channel
close(ch)

// ❌ NUNCA cierres un channel desde el receiver
// ❌ NUNCA cierres un channel dos veces (panic)
// ❌ NUNCA envíes a un channel cerrado (panic)

// Enviar a channel cerrado = PANIC
ch := make(chan int)
close(ch)
ch <- 1  // panic: send on closed channel

Iterar sobre Channels con Range

package main

import "fmt"

func generador(n int) <-chan int {
    ch := make(chan int)

    go func() {
        for i := 0; i < n; i++ {
            ch <- i
        }
        close(ch)  // ¡Importante! Permite que range termine
    }()

    return ch
}

func main() {
    // Range itera hasta que el channel se cierra
    for num := range generador(5) {
        fmt.Println(num)
    }
    fmt.Println("Generador agotado")
}

Salida:

0
1
2
3
4
Generador agotado

Channels Direccionales

Puedes restringir un channel a solo-envío o solo-recepción. Esto hace el código más seguro y auto-documentado.

package main

import "fmt"

// Esta función SOLO puede enviar al channel
func productor(ch chan<- int) {
    for i := 0; i < 5; i++ {
        ch <- i
    }
    close(ch)
}

// Esta función SOLO puede recibir del channel
func consumidor(ch <-chan int) {
    for num := range ch {
        fmt.Println("Recibido:", num)
    }
}

func main() {
    ch := make(chan int)  // Bidireccional

    go productor(ch)  // Se convierte a chan<- int automáticamente
    consumidor(ch)    // Se convierte a <-chan int automáticamente
}

Ejemplo Práctico: Pipeline de Procesamiento

Un pipeline es una cadena de etapas donde cada etapa es una goroutine que procesa datos y los pasa a la siguiente.

package main

import "fmt"

// Etapa 1: Genera números
func generar(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

// Etapa 2: Eleva al cuadrado
func cuadrado(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

// Etapa 3: Suma todos
func sumar(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        total := 0
        for n := range in {
            total += n
        }
        out <- total
        close(out)
    }()
    return out
}

func main() {
    // Pipeline: generar -> cuadrado -> sumar
    //           1,2,3,4 -> 1,4,9,16 -> 30

    resultado := sumar(cuadrado(generar(1, 2, 3, 4)))
    fmt.Println("Resultado:", <-resultado)  // 30
}
flowchart LR
    A[generar<br>1,2,3,4] --> B[cuadrado<br>1,4,9,16]
    B --> C[sumar<br>30]

    style A fill:#22c55e,color:#fff
    style B fill:#3b82f6,color:#fff
    style C fill:#8b5cf6,color:#fff

Errores Comunes con Channels

// ❌ ERROR 1: Deadlock - enviar sin receptor
func main() {
    ch := make(chan int)
    ch <- 1  // DEADLOCK: nadie puede recibir
}

// ❌ ERROR 2: Deadlock - recibir sin emisor
func main() {
    ch := make(chan int)
    <-ch  // DEADLOCK: nadie enviará
}

// ❌ ERROR 3: Channel nil
func main() {
    var ch chan int  // nil
    ch <- 1          // BLOQUEA PARA SIEMPRE
    <-ch             // BLOQUEA PARA SIEMPRE
}

// ✅ CORRECTO: Siempre inicializa
func main() {
    ch := make(chan int)
    // usar ch
}

// ❌ ERROR 4: Olvidar cerrar en range
func main() {
    ch := make(chan int)
    go func() {
        ch <- 1
        ch <- 2
        // olvidó close(ch)
    }()
    for n := range ch {  // DEADLOCK después de recibir 1 y 2
        fmt.Println(n)
    }
}

Detectar Deadlock

Go detecta deadlocks globales automáticamente:

package main

func main() {
    ch := make(chan int)
    ch <- 1
}

Salida:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
    /path/main.go:5 +0x...

Pero no detecta deadlocks parciales:

func main() {
    ch := make(chan int)
    go func() {
        ch <- 1
        ch <- 2  // Bloquea aquí si main solo lee una vez
    }()
    fmt.Println(<-ch)  // Lee 1
    // La goroutine queda bloqueada intentando enviar 2
    // El programa termina sin error
}

📊 Resumen de la Parte 3

OperaciónSintaxisComportamiento
Crear unbufferedmake(chan T)Bloquea hasta match
Crear bufferedmake(chan T, n)Buffer de n elementos
Enviarch <- vBloquea si lleno/unbuffered
Recibirv := <-chBloquea si vacío
Cerrarclose(ch)Solo el sender
Iterarfor v := range chHasta que cierre
flowchart TD
    subgraph Channels["Lo que aprendiste"]
        A[make chan] --> B[Enviar/Recibir]
        B --> C[Buffered vs Unbuffered]
        C --> D[close y range]
        D --> E[Direccionales]
        E --> F[Pipelines]
    end

    F --> G[⚡ Listo para Select]

    style G fill:#f59e0b,color:#fff

Los channels son poderosos, pero ¿qué pasa cuando necesitas escuchar múltiples channels a la vez?

La respuesta: Select.


⚡ Parte 4: Select - Multiplexando Channels

¿Qué es Select?

select es como un switch pero para operaciones de channels. Permite esperar en múltiples channels simultáneamente y ejecutar el caso que esté listo primero.

Analogía del Aeropuerto:

Imagina que estás en un aeropuerto esperando tu vuelo:

- Puerta A: Vuelo a Madrid
- Puerta B: Vuelo a París
- Puerta C: Anuncio de retrasos

Un 'select' es como tener ojos en las 3 puertas:
- Reaccionas al PRIMERO que tenga actividad
- No te quedas "pegado" esperando solo una puerta

Sintaxis Básica

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "mensaje de canal 1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "mensaje de canal 2"
    }()

    // Select espera en ambos channels
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("Recibido de ch1:", msg1)
        case msg2 := <-ch2:
            fmt.Println("Recibido de ch2:", msg2)
        }
    }
}

Salida:

Recibido de ch1: mensaje de canal 1
Recibido de ch2: mensaje de canal 2

Comportamiento de Select

select {
case <-ch1:
    // Se ejecuta si ch1 tiene datos
case ch2 <- valor:
    // Se ejecuta si ch2 puede recibir
case val := <-ch3:
    // Recibe y asigna
default:
    // Se ejecuta si ningún otro caso está listo (non-blocking)
}

Reglas:

  1. Si ningún caso está listo → bloquea (sin default)
  2. Si un caso está listo → lo ejecuta
  3. Si múltiples casos están listos → elige uno al azar
  4. Si hay default y ningún caso listo → ejecuta default

Select Non-Blocking con Default

package main

import "fmt"

func main() {
    ch := make(chan int)

    // Intenta recibir sin bloquear
    select {
    case val := <-ch:
        fmt.Println("Recibido:", val)
    default:
        fmt.Println("No hay datos disponibles")
    }

    // Intenta enviar sin bloquear
    select {
    case ch <- 42:
        fmt.Println("Enviado: 42")
    default:
        fmt.Println("No se pudo enviar (nadie escucha)")
    }
}

Salida:

No hay datos disponibles
No se pudo enviar (nadie escucha)

Timeout con Select

Uno de los usos más comunes de select es implementar timeouts:

package main

import (
    "fmt"
    "time"
)

func operacionLenta() <-chan string {
    ch := make(chan string)
    go func() {
        time.Sleep(3 * time.Second)  // Simula operación lenta
        ch <- "resultado"
    }()
    return ch
}

func main() {
    resultado := operacionLenta()

    select {
    case res := <-resultado:
        fmt.Println("Éxito:", res)
    case <-time.After(2 * time.Second):
        fmt.Println("Timeout: la operación tardó demasiado")
    }
}

Salida:

Timeout: la operación tardó demasiado
sequenceDiagram
    participant M as Main
    participant OP as operacionLenta
    participant T as Timer

    M->>OP: Inicia operación
    M->>T: Inicia timer (2s)

    Note over OP: Trabajando...
    T->>M: ¡Timeout!
    M->>M: Ejecuta case timeout

    Note over OP: Sigue trabajando (ignorado)

Ticker y Timer

Go proporciona time.Ticker y time.Timer que se integran con select:

package main

import (
    "fmt"
    "time"
)

func main() {
    // Timer: dispara UNA vez después de la duración
    timer := time.NewTimer(2 * time.Second)

    // Ticker: dispara REPETIDAMENTE cada duración
    ticker := time.NewTicker(500 * time.Millisecond)

    done := make(chan bool)

    go func() {
        time.Sleep(2500 * time.Millisecond)
        done <- true
    }()

    for {
        select {
        case <-ticker.C:
            fmt.Println("Tick")
        case <-timer.C:
            fmt.Println("Timer disparado!")
        case <-done:
            ticker.Stop()
            fmt.Println("Terminando")
            return
        }
    }
}

Salida:

Tick
Tick
Tick
Tick
Timer disparado!
Tick
Terminando

Select Infinito con For

El patrón más común es for + select:

package main

import (
    "fmt"
    "time"
)

func main() {
    datos := make(chan int)
    salir := make(chan bool)

    // Productor
    go func() {
        for i := 1; i <= 5; i++ {
            datos <- i
            time.Sleep(500 * time.Millisecond)
        }
        salir <- true
    }()

    // Consumidor con select infinito
    for {
        select {
        case d := <-datos:
            fmt.Println("Dato recibido:", d)
        case <-salir:
            fmt.Println("Señal de salida recibida")
            return
        }
    }
}

Caso Especial: nil Channels en Select

Un channel nil en select se ignora (nunca está listo):

package main

import "fmt"

func main() {
    ch1 := make(chan int)
    var ch2 chan int  // nil

    go func() {
        ch1 <- 1
    }()

    select {
    case v := <-ch1:
        fmt.Println("ch1:", v)
    case v := <-ch2:
        // NUNCA se ejecuta (ch2 es nil)
        fmt.Println("ch2:", v)
    }
}

Uso práctico: Puedes “desactivar” un channel estableciéndolo a nil:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        for i := 0; i < 3; i++ {
            time.Sleep(100 * time.Millisecond)
            ch1 <- i
        }
        close(ch1)
    }()

    go func() {
        for i := 10; i < 13; i++ {
            time.Sleep(150 * time.Millisecond)
            ch2 <- i
        }
        close(ch2)
    }()

    for ch1 != nil || ch2 != nil {
        select {
        case v, ok := <-ch1:
            if !ok {
                ch1 = nil  // Desactivar este case
                fmt.Println("ch1 cerrado")
                continue
            }
            fmt.Println("ch1:", v)
        case v, ok := <-ch2:
            if !ok {
                ch2 = nil  // Desactivar este case
                fmt.Println("ch2 cerrado")
                continue
            }
            fmt.Println("ch2:", v)
        }
    }
    fmt.Println("Ambos channels cerrados")
}

Ejemplo Práctico: Worker con Graceful Shutdown

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func worker(id int, jobs <-chan int, quit <-chan struct{}) {
    for {
        select {
        case job := <-jobs:
            fmt.Printf("Worker %d procesando job %d\n", id, job)
            time.Sleep(500 * time.Millisecond)
        case <-quit:
            fmt.Printf("Worker %d: recibida señal de shutdown\n", id)
            return
        }
    }
}

func main() {
    jobs := make(chan int, 100)
    quit := make(chan struct{})

    // Iniciar workers
    for i := 1; i <= 3; i++ {
        go worker(i, jobs, quit)
    }

    // Escuchar señales del SO (Ctrl+C)
    signals := make(chan os.Signal, 1)
    signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)

    // Enviar algunos jobs
    go func() {
        for i := 1; i <= 10; i++ {
            jobs <- i
        }
    }()

    // Esperar señal de terminación
    <-signals
    fmt.Println("\nRecibida señal de interrupción, iniciando shutdown...")

    // Notificar a todos los workers
    close(quit)

    time.Sleep(time.Second)  // Dar tiempo a los workers
    fmt.Println("Shutdown completado")
}

Prioridad en Select

select no tiene prioridad inherente, pero puedes simularla:

package main

import "fmt"

func main() {
    high := make(chan int, 10)
    low := make(chan int, 10)

    // Llenar ambos
    for i := 0; i < 5; i++ {
        high <- i
        low <- i + 100
    }

    // Prioridad: high sobre low
    for i := 0; i < 10; i++ {
        select {
        case v := <-high:
            fmt.Println("HIGH:", v)
        default:
            select {
            case v := <-low:
                fmt.Println("LOW:", v)
            default:
                fmt.Println("Nada que procesar")
            }
        }
    }
}

📊 Resumen de la Parte 4

PatrónUso
select básicoEsperar múltiples channels
select + defaultNon-blocking check
select + time.AfterTimeout
select + ticker.COperaciones periódicas
for + selectLoop infinito de eventos
nil channel en selectDesactivar un case
flowchart TD
    subgraph Select["Lo que aprendiste"]
        A[select básico] --> B[default non-blocking]
        B --> C[Timeouts]
        C --> D[Tickers/Timers]
        D --> E[for+select]
        E --> F[Graceful shutdown]
    end

    F --> G[🎛️ Listo para Context]

    style G fill:#8b5cf6,color:#fff

Ahora que dominas channels y select, es hora de aprender la herramienta más importante para control de flujo concurrente: Context.


🎛️ Parte 5: Context - Control de Flujo Concurrente

¿Qué es Context?

El paquete context es la solución estándar de Go para:

  1. Cancelación: Detener operaciones cuando ya no son necesarias
  2. Timeouts/Deadlines: Límites de tiempo en operaciones
  3. Propagación de valores: Pasar datos request-scoped

Analogía del Director de Orquesta:

Imagina una orquesta:

- El director (context padre) puede parar la música
- Cuando el director para, TODOS los músicos paran
- Cada sección puede tener su propio sub-director
- Si el director principal para, todos los sub-directores paran también

Context funciona igual:
- Un context padre cancelado → todos los hijos cancelados
- La cancelación se propaga hacia abajo en el árbol

El Árbol de Contexts

flowchart TD
    BG[context.Background] --> CTX1[ctx con timeout]
    CTX1 --> CTX2[ctx con cancel]
    CTX1 --> CTX3[ctx con valor]
    CTX2 --> CTX4[ctx hijo 1]
    CTX2 --> CTX5[ctx hijo 2]

    style BG fill:#6b7280,color:#fff
    style CTX1 fill:#3b82f6,color:#fff
    style CTX2 fill:#ef4444,color:#fff
    style CTX3 fill:#10b981,color:#fff

Crear Contexts

package main

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

func main() {
    // 1. Context raíz (nunca se cancela)
    ctx := context.Background()

    // 2. Context vacío (para cuando no sabes qué context usar)
    _ = context.TODO()

    // 3. Context con cancelación manual
    ctxCancel, cancel := context.WithCancel(ctx)
    defer cancel()  // SIEMPRE llama cancel para liberar recursos

    // 4. Context con timeout (se cancela automáticamente)
    ctxTimeout, cancelTimeout := context.WithTimeout(ctx, 5*time.Second)
    defer cancelTimeout()

    // 5. Context con deadline (tiempo absoluto)
    deadline := time.Now().Add(5 * time.Second)
    ctxDeadline, cancelDeadline := context.WithDeadline(ctx, deadline)
    defer cancelDeadline()

    // 6. Context con valor
    ctxValue := context.WithValue(ctx, "userID", "12345")

    fmt.Println("Contexts creados")
    fmt.Println("Valor en ctxValue:", ctxValue.Value("userID"))

    // Usar el context...
    _ = ctxCancel
    _ = ctxTimeout
    _ = ctxDeadline
}

Regla de Oro: Siempre Llamar Cancel

// ✅ CORRECTO: defer cancel() inmediatamente después de crear
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel()  // Libera recursos cuando la función termina

// ❌ INCORRECTO: Olvidar cancel = memory leak
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
// cancel nunca se llama = goroutine del timer sigue viva

Cancelación Manual

package main

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

func operacion(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Operación %d: cancelada (%v)\n", id, ctx.Err())
            return
        default:
            fmt.Printf("Operación %d: trabajando...\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    // Iniciar 3 operaciones
    for i := 1; i <= 3; i++ {
        go operacion(ctx, i)
    }

    // Esperar 2 segundos y cancelar
    time.Sleep(2 * time.Second)
    fmt.Println("\n--- Cancelando todas las operaciones ---\n")
    cancel()

    // Dar tiempo para ver los mensajes de cancelación
    time.Sleep(100 * time.Millisecond)
}

Salida:

Operación 1: trabajando...
Operación 2: trabajando...
Operación 3: trabajando...
Operación 1: trabajando...
Operación 3: trabajando...
Operación 2: trabajando...
Operación 2: trabajando...
Operación 1: trabajando...
Operación 3: trabajando...

--- Cancelando todas las operaciones ---

Operación 1: cancelada (context canceled)
Operación 3: cancelada (context canceled)
Operación 2: cancelada (context canceled)

Timeout Automático

package main

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

func fetchData(ctx context.Context) (string, error) {
    // Simula una operación que tarda 3 segundos
    select {
    case <-time.After(3 * time.Second):
        return "datos obtenidos", nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    // Timeout de 2 segundos (la operación tarda 3)
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    fmt.Println("Iniciando fetch...")
    start := time.Now()

    data, err := fetchData(ctx)

    if err != nil {
        fmt.Printf("Error después de %v: %v\n", time.Since(start), err)
        return
    }
    fmt.Println("Datos:", data)
}

Salida:

Iniciando fetch...
Error después de 2s: context deadline exceeded

Deadline vs Timeout

package main

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

func main() {
    ctx := context.Background()

    // Timeout: "en 5 segundos desde ahora"
    ctxTimeout, cancel1 := context.WithTimeout(ctx, 5*time.Second)
    defer cancel1()

    // Deadline: "a las 15:30:00 exactamente"
    deadline := time.Date(2026, 1, 13, 15, 30, 0, 0, time.Local)
    ctxDeadline, cancel2 := context.WithDeadline(ctx, deadline)
    defer cancel2()

    // Ambos tienen Deadline() method
    if d, ok := ctxTimeout.Deadline(); ok {
        fmt.Println("Timeout deadline:", d)
    }
    if d, ok := ctxDeadline.Deadline(); ok {
        fmt.Println("Deadline deadline:", d)
    }
}

Context con Valores

Los valores en context son útiles para datos request-scoped como IDs de traza, tokens de autenticación, etc.

package main

import (
    "context"
    "fmt"
)

// Usar tipos propios como keys (evita colisiones)
type contextKey string

const (
    userIDKey    contextKey = "userID"
    requestIDKey contextKey = "requestID"
)

func middleware(ctx context.Context) context.Context {
    // Agregar valores al context
    ctx = context.WithValue(ctx, userIDKey, "user-123")
    ctx = context.WithValue(ctx, requestIDKey, "req-456")
    return ctx
}

func handler(ctx context.Context) {
    // Extraer valores del context
    userID := ctx.Value(userIDKey)
    requestID := ctx.Value(requestIDKey)

    fmt.Printf("Handler: userID=%v, requestID=%v\n", userID, requestID)
}

func main() {
    ctx := context.Background()
    ctx = middleware(ctx)
    handler(ctx)
}

Importante sobre valores en context:

// ✅ CORRECTO: Usar para datos request-scoped
ctx = context.WithValue(ctx, requestIDKey, "req-123")

// ❌ INCORRECTO: Usar para pasar parámetros de función
ctx = context.WithValue(ctx, "dbConnection", db)  // Mal: pásalo como parámetro

// ❌ INCORRECTO: Usar tipos primitivos como keys
ctx = context.WithValue(ctx, "userID", "123")  // Riesgo de colisión

Patrón: Context como Primer Parámetro

En Go, el context siempre es el primer parámetro de una función:

// ✅ CORRECTO: ctx es el primer parámetro
func FetchUser(ctx context.Context, userID string) (*User, error)
func ProcessOrder(ctx context.Context, order Order) error
func SendEmail(ctx context.Context, to, subject, body string) error

// ❌ INCORRECTO: ctx no es el primero
func FetchUser(userID string, ctx context.Context) (*User, error)

// ❌ INCORRECTO: ctx en un struct
type Request struct {
    Ctx context.Context  // No hagas esto
    Data string
}

Ejemplo Práctico: HTTP Handler con Context

package main

import (
    "context"
    "fmt"
    "net/http"
    "time"
)

func fetchFromDB(ctx context.Context) (string, error) {
    // Simula una query lenta
    select {
    case <-time.After(2 * time.Second):
        return "datos de la DB", nil
    case <-ctx.Done():
        return "", fmt.Errorf("DB query cancelada: %w", ctx.Err())
    }
}

func handler(w http.ResponseWriter, r *http.Request) {
    // El request ya tiene un context (se cancela si el cliente cierra conexión)
    ctx := r.Context()

    // Agregar timeout adicional
    ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
    defer cancel()

    data, err := fetchFromDB(ctx)
    if err != nil {
        http.Error(w, err.Error(), http.StatusGatewayTimeout)
        return
    }

    fmt.Fprintf(w, "Datos: %s", data)
}

func main() {
    http.HandleFunc("/", handler)
    fmt.Println("Servidor en :8080")
    http.ListenAndServe(":8080", nil)
}

Context en Cadenas de Llamadas

package main

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

func operacionA(ctx context.Context) error {
    fmt.Println("A: iniciando")

    // Verificar cancelación antes de continuar
    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }

    // Llamar a B
    if err := operacionB(ctx); err != nil {
        return fmt.Errorf("A falló: %w", err)
    }

    fmt.Println("A: completada")
    return nil
}

func operacionB(ctx context.Context) error {
    fmt.Println("B: iniciando")

    // Simular trabajo
    select {
    case <-time.After(1 * time.Second):
        fmt.Println("B: trabajo completado")
    case <-ctx.Done():
        return ctx.Err()
    }

    // Llamar a C
    if err := operacionC(ctx); err != nil {
        return fmt.Errorf("B falló: %w", err)
    }

    fmt.Println("B: completada")
    return nil
}

func operacionC(ctx context.Context) error {
    fmt.Println("C: iniciando")

    // Simular trabajo largo
    select {
    case <-time.After(5 * time.Second):
        fmt.Println("C: trabajo completado")
    case <-ctx.Done():
        fmt.Println("C: cancelada")
        return ctx.Err()
    }

    fmt.Println("C: completada")
    return nil
}

func main() {
    // Timeout de 3 segundos (C tarda 5)
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    err := operacionA(ctx)
    if err != nil {
        fmt.Println("Error:", err)
    }
}

Salida:

A: iniciando
B: iniciando
B: trabajo completado
C: iniciando
C: cancelada
Error: A falló: B falló: context deadline exceeded

Context.Cause (Go 1.20+)

Desde Go 1.20 (estable en Go 1.25), puedes proporcionar una causa personalizada para la cancelación:

package main

import (
    "context"
    "errors"
    "fmt"
)

var ErrShutdown = errors.New("servidor apagándose")

func main() {
    ctx, cancel := context.WithCancelCause(context.Background())

    // Cancelar con causa personalizada
    cancel(ErrShutdown)

    // Obtener la causa
    fmt.Println("Error:", ctx.Err())           // context canceled
    fmt.Println("Causa:", context.Cause(ctx))  // servidor apagándose
}

AfterFunc (Go 1.21+)

Desde Go 1.21 (mejorado en Go 1.25), context.AfterFunc ejecuta código cuando un context se cancela:

package main

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

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    // Registrar cleanup que se ejecuta al cancelar
    stop := context.AfterFunc(ctx, func() {
        fmt.Println("Cleanup: liberando recursos...")
    })

    // Si queremos cancelar el AfterFunc antes
    _ = stop  // stop() cancelaría el callback

    // Esperar a que expire el timeout
    <-ctx.Done()

    time.Sleep(100 * time.Millisecond)  // Dar tiempo al callback
}

WithoutCancel (Go 1.21+)

Crea un context que NO se cancela cuando el padre se cancela, pero mantiene los valores:

package main

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

func backgroundTask(ctx context.Context) {
    // Este context NO se cancela cuando el padre se cancela
    ctx = context.WithoutCancel(ctx)

    for i := 0; i < 5; i++ {
        select {
        case <-ctx.Done():
            fmt.Println("Cancelado")
            return
        default:
            fmt.Printf("Trabajando... (iteración %d)\n", i)
            time.Sleep(500 * time.Millisecond)
        }
    }
    fmt.Println("Tarea completada")
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)

    go backgroundTask(ctx)

    time.Sleep(500 * time.Millisecond)
    cancel()  // Cancelamos, pero backgroundTask continúa

    time.Sleep(3 * time.Second)
}

Nuevas Características de Go 1.23-1.25

Go 1.25 incluye mejoras significativas en concurrencia:

package main

import (
    "fmt"
    "iter"
    "slices"
)

// Go 1.23+: Iteradores con range-over-func
func Fibonacci(n int) iter.Seq[int] {
    return func(yield func(int) bool) {
        a, b := 0, 1
        for i := 0; i < n; i++ {
            if !yield(a) {
                return
            }
            a, b = b, a+b
        }
    }
}

// Go 1.23+: Iteradores sobre channels (iter.Pull)
func ChannelToIterator[T any](ch <-chan T) iter.Seq[T] {
    return func(yield func(T) bool) {
        for v := range ch {
            if !yield(v) {
                return
            }
        }
    }
}

func main() {
    // Range-over-func con iteradores
    fmt.Println("Fibonacci:")
    for n := range Fibonacci(10) {
        fmt.Print(n, " ")
    }
    fmt.Println()

    // Usar slices.Collect para convertir iterador a slice
    fibs := slices.Collect(Fibonacci(10))
    fmt.Println("Como slice:", fibs)
}

unique Package (Go 1.23+)

Deduplicación eficiente de valores para reducir memoria:

package main

import (
    "fmt"
    "unique"
)

func main() {
    // Handle es un identificador único para valores iguales
    h1 := unique.Make("hello")
    h2 := unique.Make("hello")
    h3 := unique.Make("world")

    // Comparten la misma memoria subyacente
    fmt.Println("h1 == h2:", h1 == h2)  // true
    fmt.Println("h1 == h3:", h1 == h3)  // false

    // Recuperar el valor
    fmt.Println("Valor:", h1.Value())

    // Útil para string interning en sistemas concurrentes
    type User struct {
        ID       int
        Role     unique.Handle[string]  // Roles compartidos eficientemente
    }

    admin := unique.Make("admin")
    users := []User{
        {1, admin},
        {2, admin},  // Comparte la misma referencia
        {3, unique.Make("user")},
    }

    for _, u := range users {
        fmt.Printf("User %d: %s\n", u.ID, u.Role.Value())
    }
}

Mejoras en sync (Go 1.24-1.25)

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    // Go 1.19+: Tipos atómicos genéricos (ahora estándar)
    var counter atomic.Int64
    counter.Add(10)
    fmt.Println("Counter:", counter.Load())

    // atomic.Pointer[T] para punteros atómicos type-safe
    type Config struct {
        MaxConn int
        Timeout int
    }

    var currentConfig atomic.Pointer[Config]
    currentConfig.Store(&Config{MaxConn: 100, Timeout: 30})

    // Lectura atómica sin locks
    cfg := currentConfig.Load()
    fmt.Printf("Config: MaxConn=%d\n", cfg.MaxConn)

    // CompareAndSwap para actualizaciones condicionales
    oldCfg := cfg
    newCfg := &Config{MaxConn: 200, Timeout: 60}
    swapped := currentConfig.CompareAndSwap(oldCfg, newCfg)
    fmt.Println("Config actualizada:", swapped)

    // sync.OnceValue y sync.OnceValues (Go 1.21+)
    getValue := sync.OnceValue(func() int {
        fmt.Println("Calculando valor...")
        return 42
    })

    // Solo calcula una vez, múltiples llamadas
    fmt.Println(getValue())
    fmt.Println(getValue())
    fmt.Println(getValue())
}

Structured Concurrency Pattern (Go 1.25 idiomático)

Go 1.25 promueve un patrón más estructurado para concurrencia:

package main

import (
    "context"
    "errors"
    "fmt"
    "time"

    "golang.org/x/sync/errgroup"
)

// Scope define un ámbito de concurrencia estructurada
type Scope struct {
    g   *errgroup.Group
    ctx context.Context
}

func NewScope(ctx context.Context) (*Scope, context.Context) {
    g, ctx := errgroup.WithContext(ctx)
    return &Scope{g: g, ctx: ctx}, ctx
}

func (s *Scope) Go(fn func(context.Context) error) {
    s.g.Go(func() error {
        return fn(s.ctx)
    })
}

func (s *Scope) Wait() error {
    return s.g.Wait()
}

// Ejemplo de uso
func fetchUserData(ctx context.Context, userID int) error {
    scope, ctx := NewScope(ctx)

    var profile, orders, notifications string

    scope.Go(func(ctx context.Context) error {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-time.After(100 * time.Millisecond):
            profile = fmt.Sprintf("Profile-%d", userID)
            return nil
        }
    })

    scope.Go(func(ctx context.Context) error {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-time.After(150 * time.Millisecond):
            orders = fmt.Sprintf("Orders-%d", userID)
            return nil
        }
    })

    scope.Go(func(ctx context.Context) error {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-time.After(80 * time.Millisecond):
            notifications = fmt.Sprintf("Notifications-%d", userID)
            return nil
        }
    })

    if err := scope.Wait(); err != nil {
        return err
    }

    fmt.Printf("User %d: %s, %s, %s\n", userID, profile, orders, notifications)
    return nil
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := fetchUserData(ctx, 123); err != nil {
        fmt.Println("Error:", err)
    }
}

📊 Resumen de la Parte 5

FunciónUso
context.Background()Context raíz para main/init
context.TODO()Placeholder temporal
context.WithCancelCancelación manual
context.WithTimeoutTimeout relativo
context.WithDeadlineDeadline absoluto
context.WithValueDatos request-scoped
context.WithCancelCauseCancelación con causa
context.WithoutCancel (1.21)Context que no hereda cancel
context.AfterFunc (1.21)Callback al cancelar

Novedades Go 1.23-1.25:

FeatureDescripción
iter.SeqIteradores con range-over-func
unique.HandleString interning eficiente
sync.OnceValueInicialización lazy type-safe
atomic.Pointer[T]Punteros atómicos genéricos
Structured concurrencyPatrón errgroup mejorado

Reglas de uso:

  1. ✅ Context es siempre el primer parámetro
  2. ✅ Siempre llamar cancel() con defer
  3. ✅ Verificar ctx.Done() en loops largos
  4. ❌ No guardar context en structs
  5. ❌ No pasar nil como context (usa context.TODO())
flowchart TD
    subgraph Context["Lo que aprendiste"]
        A[Background/TODO] --> B[WithCancel]
        B --> C[WithTimeout]
        C --> D[WithDeadline]
        D --> E[WithValue]
        E --> F[Propagación]
    end

    F --> G[🔧 Listo para Patrones]

    style G fill:#ec4899,color:#fff

Con goroutines, channels, select y context dominados, es hora de aprender patrones de concurrencia profesionales.


🔧 Parte 6: Patrones de Concurrencia

Los patrones de concurrencia son soluciones probadas a problemas comunes. Dominarlos te convierte en un desarrollador Go profesional.

Patrón 1: Worker Pool

Un worker pool es un grupo fijo de goroutines que procesan trabajos de una cola. Controla el paralelismo y evita crear demasiadas goroutines.

flowchart LR
    Jobs[Jobs Queue] --> W1[Worker 1]
    Jobs --> W2[Worker 2]
    Jobs --> W3[Worker 3]
    W1 --> Results[Results]
    W2 --> Results
    W3 --> Results

    style Jobs fill:#3b82f6,color:#fff
    style Results fill:#10b981,color:#fff
package main

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

type Job struct {
    ID   int
    Data string
}

type Result struct {
    JobID  int
    Output string
}

func worker(ctx context.Context, id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()

    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d: shutdown\n", id)
            return
        case job, ok := <-jobs:
            if !ok {
                fmt.Printf("Worker %d: no más jobs\n", id)
                return
            }

            // Procesar el job
            fmt.Printf("Worker %d: procesando job %d\n", id, job.ID)
            time.Sleep(500 * time.Millisecond)  // Simula trabajo

            results <- Result{
                JobID:  job.ID,
                Output: fmt.Sprintf("procesado: %s", job.Data),
            }
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    const numWorkers = 3
    const numJobs = 10

    jobs := make(chan Job, numJobs)
    results := make(chan Result, numJobs)

    // Iniciar workers
    var wg sync.WaitGroup
    for i := 1; i <= numWorkers; i++ {
        wg.Add(1)
        go worker(ctx, i, jobs, results, &wg)
    }

    // Enviar jobs
    for i := 1; i <= numJobs; i++ {
        jobs <- Job{ID: i, Data: fmt.Sprintf("dato-%d", i)}
    }
    close(jobs)  // Señala que no hay más jobs

    // Esperar workers y cerrar results
    go func() {
        wg.Wait()
        close(results)
    }()

    // Recolectar resultados
    for result := range results {
        fmt.Printf("Resultado: Job %d -> %s\n", result.JobID, result.Output)
    }
}

Patrón 2: Fan-Out / Fan-In

Fan-Out: Distribuir trabajo a múltiples goroutines. Fan-In: Combinar resultados de múltiples goroutines en un solo channel.

flowchart LR
    subgraph FanOut["Fan-Out"]
        Input[Input] --> G1[Goroutine 1]
        Input --> G2[Goroutine 2]
        Input --> G3[Goroutine 3]
    end

    subgraph FanIn["Fan-In"]
        G1 --> Merge[Merge]
        G2 --> Merge
        G3 --> Merge
        Merge --> Output[Output]
    end
package main

import (
    "fmt"
    "sync"
    "time"
)

// Genera números
func generador(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

// Fan-Out: Crea múltiples procesadores
func procesador(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            time.Sleep(100 * time.Millisecond)  // Simula trabajo
            out <- n * n
        }
        close(out)
    }()
    return out
}

// Fan-In: Combina múltiples channels en uno
func fanIn(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup

    // Goroutine por cada channel de entrada
    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for n := range c {
                out <- n
            }
        }(ch)
    }

    // Cerrar out cuando todos terminen
    go func() {
        wg.Wait()
        close(out)
    }()

    return out
}

func main() {
    start := time.Now()

    // Generador
    nums := generador(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

    // Fan-Out: 3 procesadores leyendo del mismo channel
    p1 := procesador(nums)
    p2 := procesador(nums)
    p3 := procesador(nums)

    // Fan-In: Combinar resultados
    resultados := fanIn(p1, p2, p3)

    // Consumir resultados
    for r := range resultados {
        fmt.Println(r)
    }

    fmt.Printf("Tiempo total: %v\n", time.Since(start))
}

Patrón 3: Pipeline

Un pipeline es una serie de etapas conectadas por channels, donde cada etapa procesa y pasa datos a la siguiente.

package main

import (
    "context"
    "fmt"
)

// Etapa 1: Generar números
func generate(ctx context.Context, nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for _, n := range nums {
            select {
            case <-ctx.Done():
                return
            case out <- n:
            }
        }
    }()
    return out
}

// Etapa 2: Elevar al cuadrado
func square(ctx context.Context, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            select {
            case <-ctx.Done():
                return
            case out <- n * n:
            }
        }
    }()
    return out
}

// Etapa 3: Filtrar pares
func filterEven(ctx context.Context, in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for n := range in {
            if n%2 == 0 {
                select {
                case <-ctx.Done():
                    return
                case out <- n:
                }
            }
        }
    }()
    return out
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // Pipeline: generate -> square -> filterEven
    // 1,2,3,4,5 -> 1,4,9,16,25 -> 4,16

    for n := range filterEven(ctx, square(ctx, generate(ctx, 1, 2, 3, 4, 5))) {
        fmt.Println(n)
    }
}

Patrón 4: Semáforo (Limitar Concurrencia)

Cuando necesitas limitar cuántas goroutines ejecutan simultáneamente:

package main

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

type Semaphore chan struct{}

func NewSemaphore(size int) Semaphore {
    return make(chan struct{}, size)
}

func (s Semaphore) Acquire() {
    s <- struct{}{}
}

func (s Semaphore) Release() {
    <-s
}

func proceso(id int) {
    fmt.Printf("Proceso %d: iniciando\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Proceso %d: terminando\n", id)
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    // Máximo 3 procesos simultáneos
    sem := NewSemaphore(3)

    for i := 1; i <= 10; i++ {
        i := i

        sem.Acquire()  // Espera si ya hay 3 ejecutando

        go func() {
            defer sem.Release()

            select {
            case <-ctx.Done():
                return
            default:
                proceso(i)
            }
        }()
    }

    // Esperar a que todos terminen adquiriendo todos los slots
    for i := 0; i < cap(sem); i++ {
        sem.Acquire()
    }
    fmt.Println("Todos los procesos completados")
}

Patrón 5: Or-Done Channel

Permite leer de un channel respetando la cancelación del context:

package main

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

// orDone envuelve un channel para respetar cancelación
func orDone(ctx context.Context, c <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        defer close(out)
        for {
            select {
            case <-ctx.Done():
                return
            case v, ok := <-c:
                if !ok {
                    return
                }
                select {
                case out <- v:
                case <-ctx.Done():
                    return
                }
            }
        }
    }()
    return out
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    // Generador lento
    nums := make(chan int)
    go func() {
        defer close(nums)
        for i := 1; i <= 100; i++ {
            time.Sleep(500 * time.Millisecond)
            nums <- i
        }
    }()

    // Usar orDone para respetar timeout
    for n := range orDone(ctx, nums) {
        fmt.Println(n)
    }
    fmt.Println("Terminado (timeout o completado)")
}

Patrón 6: Errgroup (Manejo de Errores en Grupos)

El paquete golang.org/x/sync/errgroup es la forma profesional de manejar errores en grupos de goroutines:

package main

import (
    "context"
    "errors"
    "fmt"
    "time"

    "golang.org/x/sync/errgroup"
)

func fetchUser(ctx context.Context, id int) (string, error) {
    select {
    case <-time.After(100 * time.Millisecond):
        return fmt.Sprintf("User-%d", id), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func fetchOrder(ctx context.Context, id int) (string, error) {
    select {
    case <-time.After(200 * time.Millisecond):
        if id == 3 {
            return "", errors.New("order not found")
        }
        return fmt.Sprintf("Order-%d", id), nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func main() {
    ctx := context.Background()
    g, ctx := errgroup.WithContext(ctx)

    var user, order string

    g.Go(func() error {
        var err error
        user, err = fetchUser(ctx, 1)
        return err
    })

    g.Go(func() error {
        var err error
        order, err = fetchOrder(ctx, 3)  // Este fallará
        return err
    })

    // Wait retorna el primer error (si hay alguno)
    if err := g.Wait(); err != nil {
        fmt.Println("Error:", err)
        return
    }

    fmt.Printf("User: %s, Order: %s\n", user, order)
}

Patrón 7: Singleflight (Deduplicación)

Evita trabajo duplicado cuando múltiples goroutines piden lo mismo:

package main

import (
    "fmt"
    "sync"
    "time"

    "golang.org/x/sync/singleflight"
)

var group singleflight.Group

func fetchData(key string) (string, error) {
    fmt.Printf("Fetching %s (heavy operation)\n", key)
    time.Sleep(time.Second)
    return fmt.Sprintf("data-for-%s", key), nil
}

func getData(key string) (string, error) {
    // Singleflight asegura que solo una goroutine hace el fetch
    v, err, shared := group.Do(key, func() (interface{}, error) {
        return fetchData(key)
    })

    fmt.Printf("Key: %s, Shared: %t\n", key, shared)
    return v.(string), err
}

func main() {
    var wg sync.WaitGroup

    // 10 goroutines pidiendo el mismo key
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            data, _ := getData("important-key")
            fmt.Printf("Goroutine %d got: %s\n", id, data)
        }(i)
    }

    wg.Wait()
}

Salida:

Fetching important-key (heavy operation)
Key: important-key, Shared: false
Key: important-key, Shared: true
Key: important-key, Shared: true
... (todas comparten el mismo resultado)

Patrón 8: Rate Limiter

Limitar la tasa de operaciones:

package main

import (
    "fmt"
    "time"
)

func main() {
    // Rate limiter: 5 requests por segundo
    limiter := time.NewTicker(200 * time.Millisecond)
    defer limiter.Stop()

    requests := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}

    for _, req := range requests {
        <-limiter.C  // Esperar el tick
        go func(r int) {
            fmt.Printf("Request %d at %v\n", r, time.Now().Format("15:04:05.000"))
        }(req)
    }

    time.Sleep(time.Second)
}

Rate Limiter con Burst (usando buffered channel):

package main

import (
    "fmt"
    "time"
)

func main() {
    // Permite burst de 3, luego 1 por cada 200ms
    burstyLimiter := make(chan time.Time, 3)

    // Llenar el burst inicial
    for i := 0; i < 3; i++ {
        burstyLimiter <- time.Now()
    }

    // Reponer 1 cada 200ms
    go func() {
        for t := range time.Tick(200 * time.Millisecond) {
            burstyLimiter <- t
        }
    }()

    requests := []int{1, 2, 3, 4, 5, 6, 7, 8}

    for _, req := range requests {
        <-burstyLimiter
        fmt.Printf("Request %d at %v\n", req, time.Now().Format("15:04:05.000"))
    }
}

📊 Resumen de la Parte 6

PatrónUso
Worker PoolControl de paralelismo
Fan-Out/Fan-InDistribuir y combinar trabajo
PipelineProcesamiento en etapas
SemáforoLimitar concurrencia
Or-DoneChannel + cancelación
ErrgroupErrores en grupos
SingleflightDeduplicación
Rate LimiterControl de tasa
flowchart TD
    subgraph Patrones["Lo que aprendiste"]
        A[Worker Pool] --> B[Fan-Out/Fan-In]
        B --> C[Pipeline]
        C --> D[Semáforo]
        D --> E[Errgroup]
        E --> F[Singleflight]
        F --> G[Rate Limiter]
    end

    G --> H[⚠️ Race Conditions]

    style H fill:#ef4444,color:#fff

Ahora que conoces los patrones, es crucial entender el enemigo: Race Conditions.


⚠️ Parte 7: Race Conditions - El Enemigo Silencioso

¿Qué es una Race Condition?

Una race condition ocurre cuando dos o más goroutines acceden a la misma variable concurrentemente, y al menos una la modifica. El resultado depende del orden de ejecución, que es impredecible.

Analogía de la Cuenta Bancaria:

Imagina una cuenta bancaria con $100:

Goroutine A: Leer saldo ($100) → Sumar $50 → Escribir ($150)
Goroutine B: Leer saldo ($100) → Restar $30 → Escribir ($70)

Si ambas leen ANTES de que la otra escriba:
- Ambas ven $100
- A escribe $150
- B escribe $70

Resultado final: $70 (¡perdimos $50!)
Resultado esperado: $120

Race Condition en Go

package main

import (
    "fmt"
    "sync"
)

func main() {
    counter := 0
    var wg sync.WaitGroup

    // 1000 goroutines incrementando el mismo counter
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter++  // ¡RACE CONDITION!
        }()
    }

    wg.Wait()
    fmt.Println("Counter:", counter)
    // Resultado: Probablemente < 1000 (debería ser 1000)
}

¿Por qué counter++ no es atómico?

counter++ se traduce a:
1. Leer counter de memoria
2. Incrementar el valor
3. Escribir counter a memoria

Si G1 y G2 hacen esto simultáneamente:
G1: Lee 5    G2: Lee 5
G1: +1 = 6   G2: +1 = 6
G1: Escribe 6  G2: Escribe 6

Resultado: 6 (debería ser 7)

El Race Detector de Go

Go tiene un detector de races incorporado. Es una de las mejores herramientas para encontrar bugs de concurrencia:

# Ejecutar con detector de races
go run -race main.go

# Testear con detector de races
go test -race ./...

# Compilar con detector de races
go build -race -o myapp

Ejemplo de salida del race detector:

==================
WARNING: DATA RACE
Write at 0x00c0000140a8 by goroutine 7:
  main.main.func1()
      /path/main.go:14 +0x4a

Previous write at 0x00c0000140a8 by goroutine 6:
  main.main.func1()
      /path/main.go:14 +0x4a

Goroutine 7 (running) created at:
  main.main()
      /path/main.go:12 +0x7e

Goroutine 6 (finished) created at:
  main.main()
      /path/main.go:12 +0x7e
==================

⚠️ IMPORTANTE:

  • Usa -race en desarrollo y testing, no en producción
  • El race detector añade ~2-10x overhead
  • Solo detecta races que ocurren durante la ejecución

Solución 1: sync.Mutex

Un Mutex (mutual exclusion) asegura que solo una goroutine acceda a la sección crítica a la vez:

package main

import (
    "fmt"
    "sync"
)

func main() {
    counter := 0
    var mu sync.Mutex
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()

            mu.Lock()    // Adquirir el lock
            counter++    // Sección crítica
            mu.Unlock()  // Liberar el lock
        }()
    }

    wg.Wait()
    fmt.Println("Counter:", counter)  // Siempre 1000
}

Patrón recomendado con defer:

func incrementar(mu *sync.Mutex, counter *int) {
    mu.Lock()
    defer mu.Unlock()  // SIEMPRE libera, incluso con panic

    *counter++
}

Solución 2: sync.RWMutex

Cuando tienes muchas lecturas y pocas escrituras, un RWMutex es más eficiente:

package main

import (
    "fmt"
    "sync"
    "time"
)

type Cache struct {
    mu    sync.RWMutex
    items map[string]string
}

func NewCache() *Cache {
    return &Cache{items: make(map[string]string)}
}

func (c *Cache) Get(key string) (string, bool) {
    c.mu.RLock()         // Lock de LECTURA (múltiples lectores OK)
    defer c.mu.RUnlock()

    val, ok := c.items[key]
    return val, ok
}

func (c *Cache) Set(key, value string) {
    c.mu.Lock()          // Lock de ESCRITURA (exclusivo)
    defer c.mu.Unlock()

    c.items[key] = value
}

func main() {
    cache := NewCache()
    var wg sync.WaitGroup

    // Writer
    wg.Add(1)
    go func() {
        defer wg.Done()
        for i := 0; i < 10; i++ {
            cache.Set(fmt.Sprintf("key%d", i), fmt.Sprintf("value%d", i))
            time.Sleep(100 * time.Millisecond)
        }
    }()

    // Multiple readers
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for j := 0; j < 20; j++ {
                if val, ok := cache.Get("key5"); ok {
                    fmt.Printf("Reader %d: %s\n", id, val)
                }
                time.Sleep(50 * time.Millisecond)
            }
        }(i)
    }

    wg.Wait()
}

Solución 3: Operaciones Atómicas

Para operaciones simples sobre enteros, sync/atomic es más eficiente que mutex:

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var counter int64 = 0  // Debe ser int32 o int64
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&counter, 1)  // Operación atómica
        }()
    }

    wg.Wait()
    fmt.Println("Counter:", atomic.LoadInt64(&counter))
}

Operaciones atómicas disponibles:

// Add
atomic.AddInt64(&x, 1)   // x += 1
atomic.AddInt64(&x, -1)  // x -= 1

// Load/Store
val := atomic.LoadInt64(&x)   // Leer
atomic.StoreInt64(&x, 100)    // Escribir

// Swap
old := atomic.SwapInt64(&x, 100)  // Intercambiar

// Compare-And-Swap (CAS)
swapped := atomic.CompareAndSwapInt64(&x, old, new)

// Go 1.19+: Tipos genéricos
var counter atomic.Int64
counter.Add(1)
counter.Load()
counter.Store(100)

Solución 4: Channels (Comunicación en Lugar de Compartir)

La forma más “Go-like” de evitar races es no compartir memoria:

package main

import (
    "fmt"
)

func main() {
    counter := make(chan int, 1)
    counter <- 0  // Valor inicial

    done := make(chan bool)

    // Incrementar usando el channel como sincronización
    for i := 0; i < 1000; i++ {
        go func() {
            val := <-counter   // Tomar posesión
            val++              // Modificar
            counter <- val     // Devolver
            done <- true
        }()
    }

    // Esperar a todas
    for i := 0; i < 1000; i++ {
        <-done
    }

    fmt.Println("Counter:", <-counter)  // Siempre 1000
}

Patrón más limpio con un “actor”:

package main

import "fmt"

type Counter struct {
    value int
    inc   chan struct{}
    get   chan int
}

func NewCounter() *Counter {
    c := &Counter{
        inc: make(chan struct{}),
        get: make(chan int),
    }

    // Actor: única goroutine que toca value
    go func() {
        for {
            select {
            case <-c.inc:
                c.value++
            case c.get <- c.value:
            }
        }
    }()

    return c
}

func (c *Counter) Increment() {
    c.inc <- struct{}{}
}

func (c *Counter) Value() int {
    return <-c.get
}

func main() {
    counter := NewCounter()

    done := make(chan bool)
    for i := 0; i < 1000; i++ {
        go func() {
            counter.Increment()
            done <- true
        }()
    }

    for i := 0; i < 1000; i++ {
        <-done
    }

    fmt.Println("Counter:", counter.Value())
}

Solución 5: sync.Map (Map Concurrente)

Para maps concurrentes, usa sync.Map en lugar de map + mutex:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map
    var wg sync.WaitGroup

    // Escribir concurrentemente
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            m.Store(n, n*2)
        }(i)
    }

    wg.Wait()

    // Leer
    if val, ok := m.Load(50); ok {
        fmt.Println("Key 50:", val)
    }

    // Iterar
    m.Range(func(key, value interface{}) bool {
        fmt.Printf("%v: %v\n", key, value)
        return true  // continuar iterando
    })
}

¿Cuándo usar sync.Map vs map+mutex?

CasoRecomendación
Writes frecuentes, keys variablesmap + sync.RWMutex
Keys estables, muchas goroutinessync.Map
Cache con muchas lecturassync.Map
Cuando necesitas iterar muchomap + sync.RWMutex

Solución 6: sync.Once (Inicialización Única)

Para inicializar algo exactamente una vez:

package main

import (
    "fmt"
    "sync"
)

var (
    instance *Database
    once     sync.Once
)

type Database struct {
    connected bool
}

func GetDatabase() *Database {
    once.Do(func() {
        fmt.Println("Inicializando database...")
        instance = &Database{connected: true}
    })
    return instance
}

func main() {
    var wg sync.WaitGroup

    // 100 goroutines pidiendo la database
    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            db := GetDatabase()
            _ = db
        }()
    }

    wg.Wait()
    // "Inicializando database..." se imprime UNA vez
}

Solución 7: sync.Pool (Object Pool)

Para reutilizar objetos y reducir allocations:

package main

import (
    "bytes"
    "fmt"
    "sync"
)

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func processRequest(data string) string {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufferPool.Put(buf)
    }()

    buf.WriteString("Processed: ")
    buf.WriteString(data)

    return buf.String()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            result := processRequest(fmt.Sprintf("request-%d", n))
            _ = result
        }(i)
    }

    wg.Wait()
    fmt.Println("Done - buffers reutilizados eficientemente")
}

Errores Comunes de Sincronización

// ❌ ERROR 1: Copiar mutex
type Bad struct {
    mu sync.Mutex
    value int
}

func (b Bad) Get() int {  // Receiver por valor = copia el mutex
    b.mu.Lock()
    defer b.mu.Unlock()
    return b.value
}

// ✅ CORRECTO: Receiver por puntero
func (b *Bad) Get() int {
    b.mu.Lock()
    defer b.mu.Unlock()
    return b.value
}

// ❌ ERROR 2: Unlock sin Lock
func bad() {
    var mu sync.Mutex
    mu.Unlock()  // panic: sync: unlock of unlocked mutex
}

// ❌ ERROR 3: Doble Lock (deadlock)
func bad2() {
    var mu sync.Mutex
    mu.Lock()
    mu.Lock()  // DEADLOCK: espera a que se libere, pero nadie lo hará
}

// ❌ ERROR 4: Lock en un defer interno
func bad3() {
    var mu sync.Mutex

    func() {
        mu.Lock()
        defer mu.Unlock()
        // Si hay panic aquí, Unlock se ejecuta
    }()
    // Pero si el panic es aquí afuera, el mutex queda bloqueado
}

Debugging de Race Conditions

# 1. Ejecutar con race detector
go test -race -v ./...

# 2. Usar GODEBUG para más info
GODEBUG=schedtrace=1000 go run main.go

# 3. Compilar con race y ejecutar
go build -race -o myapp
./myapp

📊 Resumen de la Parte 7

HerramientaUso
Race Detectorgo run -race
sync.MutexExclusión mutua general
sync.RWMutexMuchas lecturas, pocas escrituras
sync/atomicOperaciones simples sobre enteros
ChannelsComunicación en lugar de compartir
sync.MapMaps concurrentes
sync.OnceInicialización única
sync.PoolReutilización de objetos

Regla de oro: Prefiere channels sobre memoria compartida. Cuando uses memoria compartida, usa el nivel más bajo de sincronización necesario.

flowchart TD
    subgraph RaceConditions["Lo que aprendiste"]
        A[Qué son races] --> B[Race Detector]
        B --> C[sync.Mutex]
        C --> D[sync.RWMutex]
        D --> E[sync/atomic]
        E --> F[Channels como sync]
        F --> G[sync.Map/Once/Pool]
    end

    G --> H[🏗️ Arquitectura Real]

    style H fill:#10b981,color:#fff

Ahora que sabes evitar races, veamos cómo aplicar todo esto en arquitectura de aplicaciones reales.


🏗️ Parte 8: Arquitectura de Aplicaciones Go Concurrentes

Principios de Diseño Concurrente

Antes de escribir código, entiende estos principios:

  1. Separación de concerns concurrentes: Cada goroutine tiene una responsabilidad clara
  2. Ownership claro: Una goroutine “posee” los datos que modifica
  3. Comunicación explícita: Channels hacen visible el flujo de datos
  4. Graceful shutdown: Siempre ten un plan para terminar limpiamente
  5. Observabilidad: Logs, métricas, tracing en puntos clave

Arquitectura: Servidor HTTP Concurrente

Un servidor HTTP profesional con worker pool, graceful shutdown y manejo de errores:

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "net/http"
    "os"
    "os/signal"
    "sync"
    "syscall"
    "time"
)

// Job representa una tarea a procesar
type Job struct {
    ID        string
    Payload   string
    ResultCh  chan<- Result
}

// Result representa el resultado de un job
type Result struct {
    JobID   string
    Output  string
    Error   error
}

// WorkerPool maneja un grupo de workers
type WorkerPool struct {
    jobs       chan Job
    numWorkers int
    wg         sync.WaitGroup
    ctx        context.Context
    cancel     context.CancelFunc
}

func NewWorkerPool(numWorkers, queueSize int) *WorkerPool {
    ctx, cancel := context.WithCancel(context.Background())
    return &WorkerPool{
        jobs:       make(chan Job, queueSize),
        numWorkers: numWorkers,
        ctx:        ctx,
        cancel:     cancel,
    }
}

func (wp *WorkerPool) Start() {
    for i := 0; i < wp.numWorkers; i++ {
        wp.wg.Add(1)
        go wp.worker(i)
    }
    log.Printf("WorkerPool iniciado con %d workers", wp.numWorkers)
}

func (wp *WorkerPool) worker(id int) {
    defer wp.wg.Done()

    for {
        select {
        case <-wp.ctx.Done():
            log.Printf("Worker %d: shutdown", id)
            return
        case job, ok := <-wp.jobs:
            if !ok {
                log.Printf("Worker %d: channel cerrado", id)
                return
            }

            log.Printf("Worker %d: procesando job %s", id, job.ID)
            result := wp.processJob(job)

            select {
            case job.ResultCh <- result:
            case <-wp.ctx.Done():
                return
            }
        }
    }
}

func (wp *WorkerPool) processJob(job Job) Result {
    // Simular procesamiento
    time.Sleep(100 * time.Millisecond)

    return Result{
        JobID:  job.ID,
        Output: fmt.Sprintf("Procesado: %s", job.Payload),
        Error:  nil,
    }
}

func (wp *WorkerPool) Submit(job Job) error {
    select {
    case wp.jobs <- job:
        return nil
    case <-wp.ctx.Done():
        return errors.New("worker pool cerrado")
    default:
        return errors.New("cola llena")
    }
}

func (wp *WorkerPool) Shutdown(timeout time.Duration) {
    log.Println("Iniciando shutdown del WorkerPool...")

    // Señalar cancelación
    wp.cancel()

    // Cerrar canal de jobs
    close(wp.jobs)

    // Esperar con timeout
    done := make(chan struct{})
    go func() {
        wp.wg.Wait()
        close(done)
    }()

    select {
    case <-done:
        log.Println("WorkerPool: shutdown completado")
    case <-time.After(timeout):
        log.Println("WorkerPool: shutdown timeout")
    }
}

// Server encapsula el servidor HTTP
type Server struct {
    server *http.Server
    pool   *WorkerPool
}

func NewServer(addr string, pool *WorkerPool) *Server {
    mux := http.NewServeMux()

    s := &Server{
        server: &http.Server{
            Addr:         addr,
            Handler:      mux,
            ReadTimeout:  5 * time.Second,
            WriteTimeout: 10 * time.Second,
            IdleTimeout:  60 * time.Second,
        },
        pool: pool,
    }

    mux.HandleFunc("/process", s.handleProcess)
    mux.HandleFunc("/health", s.handleHealth)

    return s
}

func (s *Server) handleProcess(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    // Crear job
    resultCh := make(chan Result, 1)
    job := Job{
        ID:       fmt.Sprintf("job-%d", time.Now().UnixNano()),
        Payload:  r.URL.Query().Get("data"),
        ResultCh: resultCh,
    }

    // Submitir con context del request
    ctx := r.Context()

    if err := s.pool.Submit(job); err != nil {
        http.Error(w, err.Error(), http.StatusServiceUnavailable)
        return
    }

    // Esperar resultado o cancelación
    select {
    case result := <-resultCh:
        if result.Error != nil {
            http.Error(w, result.Error.Error(), http.StatusInternalServerError)
            return
        }
        fmt.Fprintf(w, "Job %s: %s\n", result.JobID, result.Output)
    case <-ctx.Done():
        http.Error(w, "Request cancelled", http.StatusRequestTimeout)
    case <-time.After(5 * time.Second):
        http.Error(w, "Processing timeout", http.StatusGatewayTimeout)
    }
}

func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    fmt.Fprintln(w, "OK")
}

func (s *Server) Start() error {
    log.Printf("Servidor iniciando en %s", s.server.Addr)
    if err := s.server.ListenAndServe(); err != http.ErrServerClosed {
        return err
    }
    return nil
}

func (s *Server) Shutdown(ctx context.Context) error {
    log.Println("Iniciando shutdown del servidor...")
    return s.server.Shutdown(ctx)
}

func main() {
    // Crear worker pool
    pool := NewWorkerPool(5, 100)
    pool.Start()

    // Crear servidor
    server := NewServer(":8080", pool)

    // Canal para errores del servidor
    serverErrors := make(chan error, 1)
    go func() {
        serverErrors <- server.Start()
    }()

    // Escuchar señales del sistema
    shutdown := make(chan os.Signal, 1)
    signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM)

    // Esperar error o señal de shutdown
    select {
    case err := <-serverErrors:
        log.Fatalf("Error del servidor: %v", err)

    case sig := <-shutdown:
        log.Printf("Señal recibida: %v", sig)

        // Timeout para shutdown
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()

        // Shutdown del servidor
        if err := server.Shutdown(ctx); err != nil {
            log.Printf("Error en shutdown del servidor: %v", err)
            server.server.Close()
        }

        // Shutdown del worker pool
        pool.Shutdown(10 * time.Second)
    }

    log.Println("Aplicación terminada limpiamente")
}

Arquitectura: Event-Driven con Channels

Sistema de eventos desacoplado:

package main

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

// Event representa un evento del sistema
type Event struct {
    Type    string
    Payload interface{}
    Time    time.Time
}

// EventBus maneja la distribución de eventos
type EventBus struct {
    subscribers map[string][]chan Event
    mu          sync.RWMutex
    ctx         context.Context
    cancel      context.CancelFunc
}

func NewEventBus() *EventBus {
    ctx, cancel := context.WithCancel(context.Background())
    return &EventBus{
        subscribers: make(map[string][]chan Event),
        ctx:         ctx,
        cancel:      cancel,
    }
}

func (eb *EventBus) Subscribe(eventType string, bufferSize int) <-chan Event {
    eb.mu.Lock()
    defer eb.mu.Unlock()

    ch := make(chan Event, bufferSize)
    eb.subscribers[eventType] = append(eb.subscribers[eventType], ch)

    return ch
}

func (eb *EventBus) Publish(event Event) {
    eb.mu.RLock()
    defer eb.mu.RUnlock()

    subscribers := eb.subscribers[event.Type]

    for _, ch := range subscribers {
        select {
        case ch <- event:
        case <-eb.ctx.Done():
            return
        default:
            // Canal lleno, descartar evento o loguear
            fmt.Printf("Warning: subscriber lento para %s\n", event.Type)
        }
    }
}

func (eb *EventBus) Shutdown() {
    eb.cancel()

    eb.mu.Lock()
    defer eb.mu.Unlock()

    for _, subs := range eb.subscribers {
        for _, ch := range subs {
            close(ch)
        }
    }
}

// Ejemplo de uso
func main() {
    bus := NewEventBus()
    var wg sync.WaitGroup

    // Subscriber 1: Procesa eventos de usuario
    userEvents := bus.Subscribe("user.created", 10)
    wg.Add(1)
    go func() {
        defer wg.Done()
        for event := range userEvents {
            fmt.Printf("UserHandler: %+v\n", event.Payload)
        }
    }()

    // Subscriber 2: Auditoría
    auditEvents := bus.Subscribe("user.created", 10)
    wg.Add(1)
    go func() {
        defer wg.Done()
        for event := range auditEvents {
            fmt.Printf("AuditLog: %s at %v\n", event.Type, event.Time)
        }
    }()

    // Publicar eventos
    for i := 0; i < 5; i++ {
        bus.Publish(Event{
            Type:    "user.created",
            Payload: map[string]string{"id": fmt.Sprintf("user-%d", i)},
            Time:    time.Now(),
        })
    }

    time.Sleep(100 * time.Millisecond)
    bus.Shutdown()
    wg.Wait()
}

Arquitectura: Pipeline de Procesamiento de Datos

Sistema de ETL concurrente:

package main

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

// Record representa un registro de datos
type Record struct {
    ID   int
    Data string
}

// Stage representa una etapa del pipeline
type Stage func(context.Context, <-chan Record) <-chan Record

// Extract: Lee datos de una fuente
func Extract(ctx context.Context, source []Record) <-chan Record {
    out := make(chan Record)

    go func() {
        defer close(out)
        for _, r := range source {
            select {
            case <-ctx.Done():
                return
            case out <- r:
            }
        }
    }()

    return out
}

// Transform: Aplica transformación
func Transform(transformation func(Record) Record) Stage {
    return func(ctx context.Context, in <-chan Record) <-chan Record {
        out := make(chan Record)

        go func() {
            defer close(out)
            for r := range in {
                select {
                case <-ctx.Done():
                    return
                case out <- transformation(r):
                }
            }
        }()

        return out
    }
}

// TransformParallel: Transformación paralela
func TransformParallel(workers int, transformation func(Record) Record) Stage {
    return func(ctx context.Context, in <-chan Record) <-chan Record {
        out := make(chan Record)
        var wg sync.WaitGroup

        for i := 0; i < workers; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                for r := range in {
                    select {
                    case <-ctx.Done():
                        return
                    case out <- transformation(r):
                    }
                }
            }()
        }

        go func() {
            wg.Wait()
            close(out)
        }()

        return out
    }
}

// Load: Escribe a destino
func Load(ctx context.Context, in <-chan Record) []Record {
    var results []Record

    for r := range in {
        select {
        case <-ctx.Done():
            return results
        default:
            results = append(results, r)
        }
    }

    return results
}

// Pipeline: Conecta todas las etapas
func Pipeline(ctx context.Context, source []Record, stages ...Stage) []Record {
    ch := Extract(ctx, source)

    for _, stage := range stages {
        ch = stage(ctx, ch)
    }

    return Load(ctx, ch)
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // Datos de entrada
    source := make([]Record, 100)
    for i := range source {
        source[i] = Record{ID: i, Data: fmt.Sprintf("data-%d", i)}
    }

    // Definir transformaciones
    uppercase := func(r Record) Record {
        time.Sleep(10 * time.Millisecond)  // Simular trabajo
        r.Data = fmt.Sprintf("[UPPER] %s", r.Data)
        return r
    }

    enrich := func(r Record) Record {
        r.Data = fmt.Sprintf("%s (enriched)", r.Data)
        return r
    }

    // Ejecutar pipeline
    start := time.Now()
    results := Pipeline(ctx, source,
        TransformParallel(10, uppercase),  // Paralelo
        Transform(enrich),                  // Secuencial
    )

    fmt.Printf("Procesados %d registros en %v\n", len(results), time.Since(start))
}

Arquitectura: Circuit Breaker

Patrón de resiliencia para servicios externos:

package main

import (
    "context"
    "errors"
    "fmt"
    "sync"
    "time"
)

type State int

const (
    StateClosed State = iota  // Normal, requests pasan
    StateOpen                  // Fallando, requests rechazados
    StateHalfOpen             // Probando, algunos requests pasan
)

func (s State) String() string {
    return [...]string{"CLOSED", "OPEN", "HALF_OPEN"}[s]
}

type CircuitBreaker struct {
    mu               sync.RWMutex
    state            State
    failures         int
    successes        int
    failureThreshold int
    successThreshold int
    timeout          time.Duration
    lastFailure      time.Time
}

func NewCircuitBreaker(failureThreshold, successThreshold int, timeout time.Duration) *CircuitBreaker {
    return &CircuitBreaker{
        state:            StateClosed,
        failureThreshold: failureThreshold,
        successThreshold: successThreshold,
        timeout:          timeout,
    }
}

var ErrCircuitOpen = errors.New("circuit breaker is open")

func (cb *CircuitBreaker) Execute(ctx context.Context, fn func() error) error {
    if !cb.allowRequest() {
        return ErrCircuitOpen
    }

    err := fn()

    cb.recordResult(err)

    return err
}

func (cb *CircuitBreaker) allowRequest() bool {
    cb.mu.RLock()
    defer cb.mu.RUnlock()

    switch cb.state {
    case StateClosed:
        return true
    case StateOpen:
        if time.Since(cb.lastFailure) > cb.timeout {
            cb.mu.RUnlock()
            cb.mu.Lock()
            cb.state = StateHalfOpen
            cb.successes = 0
            cb.mu.Unlock()
            cb.mu.RLock()
            return true
        }
        return false
    case StateHalfOpen:
        return true
    }
    return false
}

func (cb *CircuitBreaker) recordResult(err error) {
    cb.mu.Lock()
    defer cb.mu.Unlock()

    if err != nil {
        cb.failures++
        cb.lastFailure = time.Now()
        cb.successes = 0

        if cb.failures >= cb.failureThreshold {
            cb.state = StateOpen
            fmt.Printf("Circuit OPEN: %d failures\n", cb.failures)
        }
    } else {
        cb.successes++

        if cb.state == StateHalfOpen && cb.successes >= cb.successThreshold {
            cb.state = StateClosed
            cb.failures = 0
            fmt.Println("Circuit CLOSED: service recovered")
        }
    }
}

func (cb *CircuitBreaker) State() State {
    cb.mu.RLock()
    defer cb.mu.RUnlock()
    return cb.state
}

// Ejemplo de uso
func main() {
    cb := NewCircuitBreaker(3, 2, 5*time.Second)

    // Simular llamadas fallidas
    for i := 0; i < 5; i++ {
        err := cb.Execute(context.Background(), func() error {
            return errors.New("service unavailable")
        })
        fmt.Printf("Call %d: %v, State: %s\n", i+1, err, cb.State())
    }

    fmt.Println("\nEsperando timeout...")
    time.Sleep(6 * time.Second)

    // Simular recuperación
    for i := 0; i < 3; i++ {
        err := cb.Execute(context.Background(), func() error {
            return nil  // Éxito
        })
        fmt.Printf("Recovery call %d: %v, State: %s\n", i+1, err, cb.State())
    }
}

Mejores Prácticas de Arquitectura

// 1. Siempre usar Context como primer parámetro
func ProcessData(ctx context.Context, data []byte) error

// 2. Channels con dirección explícita
func Producer(ctx context.Context) <-chan Data    // Solo envía
func Consumer(ctx context.Context, in <-chan Data) // Solo recibe

// 3. Funciones que crean goroutines deben retornar forma de pararlas
func StartWorker(ctx context.Context) (stop func())

// 4. Usar errgroup para manejo de errores en grupos
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return task1(ctx) })
g.Go(func() error { return task2(ctx) })
if err := g.Wait(); err != nil { /* handle */ }

// 5. Definir interfaces pequeñas
type Processor interface {
    Process(context.Context, Data) (Result, error)
}

// 6. Hacer el código testeable con inyección de dependencias
type Service struct {
    processor Processor
    logger    Logger
    metrics   Metrics
}

// 7. Usar options pattern para configuración
type ServerOption func(*Server)

func WithTimeout(d time.Duration) ServerOption {
    return func(s *Server) {
        s.timeout = d
    }
}

func NewServer(opts ...ServerOption) *Server {
    s := &Server{timeout: defaultTimeout}
    for _, opt := range opts {
        opt(s)
    }
    return s
}

📊 Resumen de la Parte 8

Patrón ArquitectónicoUso
Worker Pool + HTTP ServerAPIs con procesamiento background
Event BusSistemas event-driven desacoplados
Pipeline ETLProcesamiento de datos en etapas
Circuit BreakerResiliencia ante servicios externos

Principios clave:

  1. ✅ Context siempre como primer parámetro
  2. ✅ Graceful shutdown con signals
  3. ✅ Timeouts en todas las operaciones I/O
  4. ✅ Channels direccionales en APIs
  5. ✅ Separación clara de responsabilidades
flowchart TD
    subgraph Arquitectura["Lo que aprendiste"]
        A[HTTP Server] --> B[Event Bus]
        B --> C[Pipeline ETL]
        C --> D[Circuit Breaker]
        D --> E[Best Practices]
    end

    E --> F[🎓 Conclusión]

    style F fill:#6366f1,color:#fff

🎓 Conclusión: Tu Camino como Desarrollador Go

Has completado un viaje intenso desde los fundamentos hasta arquitecturas profesionales de Go concurrente.

Lo Que Aprendiste

flowchart LR
    subgraph Nivel1["Fundamentos"]
        A1[Go Básico]
        A2[Tipos y Funciones]
        A3[Structs e Interfaces]
    end

    subgraph Nivel2["Concurrencia Core"]
        B1[Goroutines]
        B2[Channels]
        B3[Select]
        B4[Context]
    end

    subgraph Nivel3["Patrones"]
        C1[Worker Pool]
        C2[Fan-Out/Fan-In]
        C3[Pipeline]
        C4[Rate Limiting]
    end

    subgraph Nivel4["Profesional"]
        D1[Race Detection]
        D2[Sync Package]
        D3[Arquitectura]
        D4[Best Practices]
    end

    Nivel1 --> Nivel2 --> Nivel3 --> Nivel4

Checklist del Desarrollador Go 1.25 Profesional

  • Entiendo la diferencia entre concurrencia y paralelismo
  • Puedo crear y sincronizar goroutines con WaitGroup
  • Sé cuándo usar channels buffered vs unbuffered
  • Uso select para multiplexar operaciones
  • Aplico Context para cancelación y timeouts
  • Uso context.WithCancelCause para errores descriptivos
  • Conozco los patrones Worker Pool, Fan-Out/Fan-In, Pipeline
  • Ejecuto siempre con -race en desarrollo
  • Uso el nivel correcto de sincronización (channels > atomic > mutex)
  • Implemento graceful shutdown en mis aplicaciones
  • Diseño APIs con channels direccionales
  • Uso iter.Seq para iteradores (Go 1.23+)
  • Aplico unique.Handle para string interning (Go 1.23+)
  • Uso sync.OnceValue para inicialización lazy (Go 1.21+)
  • Aplico structured concurrency con errgroup

Novedades de Go 1.25 para Concurrencia

// Go 1.25 optimiza el scheduler para:
// - Mejor distribución de carga entre Ps
// - Menor latencia en operaciones de channel
// - GC más eficiente con muchas goroutines

// Nuevas APIs estables en 1.25:
import (
    "iter"           // Iteradores nativos
    "unique"         // String interning
    "sync"           // OnceValue, OnceValues
    "context"        // WithoutCancel, AfterFunc
)

// Patrón moderno Go 1.25:
func ProcessItems[T any](ctx context.Context, items iter.Seq[T]) error {
    g, ctx := errgroup.WithContext(ctx)

    for item := range items {
        item := item // No necesario en Go 1.22+, pero explícito
        g.Go(func() error {
            return process(ctx, item)
        })
    }

    return g.Wait()
}

Recursos para Continuar Aprendiendo

Documentación Oficial:

Libros Recomendados:

  • “Concurrency in Go” - Katherine Cox-Buday
  • “The Go Programming Language” - Donovan & Kernighan
  • “100 Go Mistakes and How to Avoid Them” - Teiva Harsanyi
  • “Learning Go, 2nd Edition” - Jon Bodner (actualizado para Go 1.23+)

Práctica:

  • Implementa un crawler web concurrente con rate limiting
  • Crea un rate limiter distribuido con Redis
  • Construye un message broker con pub/sub
  • Desarrolla un cache con TTL, cleanup automático y sync.Pool
  • Crea un pipeline de procesamiento de imágenes con iter.Seq

Palabras Finales

Go 1.25 representa la madurez del lenguaje para sistemas concurrentes. Con cada versión, el equipo de Go ha mejorado:

  • Performance: El scheduler de Go 1.25 es 15% más eficiente que Go 1.20
  • Ergonomía: Iteradores, loop scoping, y APIs más type-safe
  • Seguridad: Mejor race detector y análisis estático

El modelo de concurrencia de Go fue diseñado para hacer la concurrencia accesible y segura. No es magia, es diseño inteligente:

  • Goroutines hacen la concurrencia barata
  • Channels hacen la comunicación explícita
  • Context hace el control de flujo manejable
  • El race detector hace los bugs visibles
  • Iteradores (Go 1.23+) hacen las secuencias elegantes

El secreto no está en memorizar patrones, sino en entender por qué existen y cuándo aplicarlos.

“Don’t communicate by sharing memory; share memory by communicating.”

Esta frase resume todo. En Go, el código concurrente más limpio es aquel donde las goroutines son actores independientes que se comunican enviándose mensajes, no compartiendo estado.

Ahora tienes las herramientas. Ve y construye algo increíble con Go 1.25.


¿Te gustó esta guía? Compártela con otros desarrolladores que quieran dominar Go concurrente.

¿Encontraste un error o tienes sugerencias? La retroalimentación es bienvenida.

Tags

#go #golang #concurrency #goroutines #channels #context #parallelism #best-practices #architecture #performance