> 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?

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