Saltar al contenido principal

Go desde cero, parte 14: sync, context, cancelación y timeouts

Curso: Go — Lección 16 de 17

> serie: aprende go de 0 a 100 | artículo 14 de 19

Los channels son la vía preferida, pero no la única. Hoy completamos la caja de herramientas con el paquete sync para sincronización clásica y con context, el mecanismo estándar para decirle a un ejército de goroutines que se acabó lo que se daba. Dominar el context en Go es media producción.


sync.WaitGroup: esperar sin dormir

El sustituto oficial de aquel time.Sleep de los artículos anteriores:

var wg sync.WaitGroup

for i := 1; i <= 5; i++ {
  wg.Add(1) // +1 ANTES de lanzar
  go func(id int) {
    defer wg.Done() // -1 al terminar, pase lo que pase
    procesa(id)
  }(i)
}

wg.Wait() // bloquea hasta que el contador llegue a 0
fmt.Println("todas terminaron")

Tres reglas: el Add va ANTES del go (si va dentro, puede que Wait pase de largo antes de que nadie sume), el Done va con defer para ejecutarse incluso con panic, y el WaitGroup no se copia (pásalo por puntero si hace falta). Documentación: pkg.go.dev/sync.

sync.Mutex: cuando compartir es inevitable

El artículo 11 predicaba comunicar antes que compartir. Pero para un contador o una cache pequeña, un mutex es más simple y rápido que montar una goroutine dueña del dato:

type ContadorSeguro struct {
  mu sync.Mutex
  n int
}

func (c *ContadorSeguro) Incrementa() {
  c.mu.Lock()
  defer c.mu.Unlock() // el dúo Lock/defer Unlock
  .n++
}

Existe también sync.RWMutex (muchos lectores, un escritor), sync.Once para inicializar exactamente una vez, y el paquete sync/atomic para operaciones atómicas sobre números. La brújula: channels para orquestar y pasar datos, mutex para proteger un estado pequeño. Y -race para confirmar que no te has engañado.

context: cancelación con modales

Imagina una petición HTTP que dispara tres consultas y el usuario cierra el navegador. ¿Quién avisa a esas goroutines? Para eso nació context.Context (blog oficial):

// Un timeout de 2 segundos para toda la operación.
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // liberar recursos SIEMPRE, incluso si sobra tiempo

resultado := make(chan string)
go consultaLenta(ctx, resultado)

select {
  case r := <-resultado:
    fmt.Println("a tiempo:", r)
  case <-ctx.Done(): // se cierra al cancelar o expirar
    fmt.Println("cancelado:", ctx.Err()) // context deadline exceeded
}

Un context forma un árbol: derivas hijos con WithCancel, WithTimeout o WithDeadline, y cancelar al padre cancela en cascada todo lo de abajo. La señal viaja por ctx.Done(), un channel que se cierra, y cerrar un channel despierta a TODOS sus receptores a la vez. Elegante, ¿no?

context en Go: Diagrama pixelart ilustrando el context en Go: un árbol con una raíz arriba, hijos WithTimeout, WithCancel y WithValue, y goroutines colgando debajo
[Cancelar el padre cancela todo lo de abajo: así se propaga un context.]

El contrato del context

La convención oficial del context en Go, según la documentación del paquete: viaja como PRIMER parámetro de toda función que cruce fronteras (red, disco, procesos largos), con el nombre ctx. No se guarda en structs, se pasa. Así queda el worker pool del artículo 13, ahora cancelable:

// El worker respetuoso comprueba ctx.Done() en su bucle:
func worker(ctx context.Context, jobs <-chan int) {
  for {
    select {
      case <-ctx.Done():
        return // fiesta terminada, recogemos
      case j, ok := <-jobs:
        if !ok {
          return
        }
        procesa(j)
    }
  }
}

Y la pieza que conecta con el mundo real: cada petición del servidor HTTP de la stdlib trae ya su context en r.Context(), que se cancela solo si el cliente corta (net/http). Tu única tarea es pasarlo hacia abajo. Lo usaremos a fondo en el proyecto (artículos 18 y 19).

Sobre context.WithValue: existe, transporta datos de la petición (IDs de trazabilidad, usuario autenticado), y la documentación pide moderación. No es un saco global para pasar parámetros que te da pereza poner en la firma.


Reto de la semana: pon a prueba el context en Go

  • Tercera vida del contador: arregla la data race del artículo 11 con sync.Mutex y compara con la versión de channels del artículo 12. ¿Cuál te parece más clara?
  • Sustituye todos los time.Sleep de espera de tus retos anteriores por WaitGroup.
  • Worker pool cancelable: añade context al reto del artículo 13 y cancela a los 500 ms. Verifica con -race y comprueba que no quedan goroutines colgadas.
  • Bonus: monta una carrera de dos «APIs» simuladas con select y un WithTimeout: gana la primera en responder, y si ninguna llega a tiempo, informa del ctx.Err().

Context en Go: nivel completado.

En el próximo artículo cambiamos de tercio: módulos, go.mod y cómo Go gestiona dependencias sin node_modules de 500 MB.

> exit
Retrato pixel art de Jenniffer Cubillos

gracias por leer