🔎 Buscar

🐹 Concurrencia en Go

Goroutines, channels, select y context explicados con código real: la base de todo backend Go de alto rendimiento.

Wiki / Apuntes📖 Contenido

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 main no espera a las goroutines. Si no sincronizas, el programa termina y las goroutines se quedan a medias. Usa sync.WaitGroup para esperar o channel para 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 := <-c te 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; el RWMutex permite 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

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.