🐹 Concurrencia en Go
Goroutines, channels, select y context explicados con código real: la base de todo backend Go de alto rendimiento.
Concurrencia en Go
Go fue diseñado para la concurrencia. Su lema: «no comuniques compartiendo memoria; comparte memoria comunicando». Todo se apoya en dos primitivas: las goroutines (unidades de ejecución ligeras) y los channels (tuberías de comunicación). Entender este modelo es la diferencia entre escribir Go que funciona y Go que escala.
Goroutines
Una goroutine es una función que se ejecuta concurrentemente con el resto. Cuesta ~2KB de pila al inicio (frente a ~1MB de un thread de SO), así que puedes lanzar miles de miles.
package main
import (
"fmt"
"sync"
"time"
)
func trabajar(id int) {
time.Sleep(100 * time.Millisecond)
fmt.Println("listo:", id)
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) { // <-- goroutine
defer wg.Done()
trabajar(id)
}(i) // pasa i por valor: evita el closure capture bug
}
wg.Wait() // espera a todas
fmt.Println("terminado")
}
⚠️ Regla nº1: el
mainno espera a las goroutines. Si no sincronizas, el programa termina y las goroutines se quedan a medias. Usasync.WaitGrouppara esperar ochannelpara coordinar.
Channels
Un channel es una tubería tipada: envías por un extremo, recibes por el otro. Por defecto es bloqueante y no bufferado: el envío bloquea hasta que alguien recibe, y viceversa. Eso es sincronización automática.
func main() {
c := make(chan int)
go func() {
c <- 42 // envía (bloquea hasta que haya receptor)
}()
v := <-c // recibe
fmt.Println(v) // 42
}
Channel bufferado
c := make(chan int, 3) // buffer de 3
c <- 1
c <- 2
c <- 3 // no bloquea: hay sitio en el buffer
// c <- 4 // bloquearía hasta que alguien reciba
Los bufferados desacoplan productor y consumidor (el clásico work queue).
Cerrar un channel
El emisor cierra (close); el receptor itera hasta que se cierra:
func produce(c chan<- int) { // chan<- : solo envío
for i := 0; i < 5; i++ {
c <- i
}
close(c)
}
func main() {
c := make(chan int)
go produce(c)
for v := range c { // recorre hasta close
fmt.Println(v)
}
}
💡 Quién cierra: solo el emisor. Recibir de un canal cerrado devuelve el valor cero sin bloquear; el ok idiomático
v, ok := <-cte dice si sigue abierto.
select: coordinar múltiples channels
select espera al primero de varios channels que esté listo. Es la herramienta para timeouts, cancelación y fan-in:
func main() {
rapido := make(chan string)
go func() { time.Sleep(50 * time.Millisecond); rapido <- "rápido" }()
select {
case msg := <-rapido:
fmt.Println("ganó:", msg)
case <-time.After(1 * time.Second):
fmt.Println("timeout") // 1s de espera máxima
}
}
Patrón clásico de timeout sobre cualquier operación:
select {
case res := <-resultado:
usar(res)
case <-time.After(3 * time.Second):
return errors.New("timeout")
}
sync primitives
Para los casos donde necesitas exclusión mutua sobre datos compartidos (raro en Go, pero existe):
type Contador struct {
mu sync.Mutex
n int
}
func (c *Contador) Inc() {
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
sync.Mutex/sync.RWMutex— exclusión mutua; elRWMutexpermite muchos lectores y un escritor.sync.WaitGroup— esperar a un grupo de goroutines.sync.Once— ejecutar algo exactamente una vez (inicialización perezosa).sync.Pool— reutilizar objetos para reducir allocations.
⚠️ Data race: si dos goroutines escriben la misma variable sin sincronización, tienes una data race (comportamiento indefinido). Siempre corre tus tests con
go test -race— te señala cada race.
context: cancelación y deadlines
context.Context es la forma estándar de propagar cancelación, deadlines y valores a través de llamadas:
func handler(ctx context.Context) {
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
select {
case <-procesar(ctx):
fmt.Println("ok")
case <-ctx.Done():
fmt.Println("cancelado:", ctx.Err())
}
}
context.Background()— raíz.context.WithTimeout/WithDeadline— límite de tiempo.context.WithCancel— cancelación manual.context.WithValue— valores (usar con moderación).
Regla: el primer parámetro de una función que puede cancelarse es ctx context.Context. Tu handler HTTP ya recibe uno; pásalo a tus repos.
El patrón worker pool
El patrón más usado en servidores reales: N goroutines consumen jobs de un channel compartido.
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
results <- j * 2
}
}
func main() {
const n = 3
jobs := make(chan int, 100)
results := make(chan int, 100)
for w := 0; w < n; w++ {
go worker(w, jobs, results) // 3 workers
}
for j := 0; j < 10; j++ {
jobs <- j
}
close(jobs)
for r := 0; r < 10; r++ {
<-results
}
}
Cheatsheet
| Quieres… | Usa… |
|---|---|
| Ejecutar algo en paralelo | go func() { ... }() |
| Esperar a un grupo | sync.WaitGroup |
| Comunicar datos | chan T |
| Coordinar N canales | select |
| Timeout | select + time.After |
| Cancelación/plazos | context |
| Exclusión mutua | sync.Mutex |
| Detectar races | go test -race |
Para profundizar
- A Tour of Go — Concurrency: interactivo y oficial.
- Concurrency in Go (Cox-Buday): el libro canónico.
- 100 Go Mistakes: errores reales de concurrencia.
- Ruta completa: Backend con Go.