> serie: aprende go de 0 a 100 | artículo 11 de 19
Todo lo anterior era el calentamiento. La concurrencia en Go es el motivo por el que el lenguaje existe y por el que probablemente estás leyendo esta serie. Y empieza con dos letras: go.
Una goroutine es una función que vuela sola
Pon go delante de cualquier llamada y esa función se ejecuta de forma concurrente, sin bloquear a quien la lanzó (A Tour of Go, goroutines):
func tarea(nombre string) {
pausa := 10 * time.Millisecond
for i := 1; i < 4; i++ {
fmt.Println(nombre, i)
time.Sleep(pausa)
}
}
func main() {
go tarea("goroutine") // la palabra go y a volar
tarea("main")
// cuando main termina, TODAS las goroutines mueren
}
Dos avisos del ejemplo:
Primero: cuando main retorna, el programa muere con todas sus goroutines dentro, terminen o no.
Segundo: ese time.Sleep para esperar es una chapuza pedagógica. La forma correcta (channels, WaitGroup) llega en los artículos 12 y 14.
Por qué no son threads
Una goroutine, la unidad básica de la concurrencia en Go, no es un thread del sistema operativo. Es mucho más barata. Arranca con una pila de unos pocos KB que crece y encoge según necesidad, y el runtime de Go multiplexa miles de ellas sobre un puñado de threads reales (Effective Go, goroutines y FAQ, why goroutines).
La FAQ oficial habla de que es práctico crear cientos de miles en el mismo espacio de direcciones. Prueba eso con threads de Java y me cuentas cómo va la factura de RAM.
Comparación con tu mundo: en Python el GIL deja la concurrencia de CPU coja y en JS el modelo es un solo hilo con event loop. En Go escribes código secuencial normal, sin async ni await ni colores de funciones, y el scheduler hace la magia.

CSP: la filosofía detrás
El modelo de concurrencia de Go viene del paper Communicating Sequential Processes de C.A.R. Hoare (1978), como reconoce la FAQ oficial. La idea: en lugar de compartir memoria y protegerla con candados, los procesos se comunican pasándose mensajes.
El eslogan oficial lo resume mejor que yo: «Do not communicate by sharing memory; instead, share memory by communicating» (Effective Go y el blog de Go). En cristiano: no le des a dos goroutines la misma variable y un mutex, dale a una el dato y que se lo pase a la otra por un channel cuando toque.
El enemigo: la data race
Esto es lo que pasa cuando se ignora el consejo anterior:
contador := 0
for i := 0; i < 1000; i++ {
go func() {
contador++ // 1000 goroutines pisándose
}()
}
time.Sleep(time.Second)
fmt.Println(contador) // ¿1000? casi nunca
contador++ son tres operaciones (leer, sumar, escribir) y mil goroutines entrelazándolas pierden incrementos por el camino. Lo peor: a veces sale bien y el bug se esconde durante meses.
Go trae detector de carreras integrado: ejecuta con go run -race . o go test -race y el runtime chivará las carreras que detecte en ejecución (documentación del race detector). Desde ya: tus tests de código concurrente llevan -race siempre.
Reto de la semana: pon a prueba la concurrencia en Go
- Lanza cinco goroutines numeradas que impriman su número tres veces y observa el entrelazado. Ejecuta varias veces: el orden cambia.
- Reproduce la data race del contador y confírmala con go run -race .. Guarda el programa: lo arreglaremos dos veces, con channels (artículo 12) y con mutex (artículo 14).
- Bonus: mide cuántas goroutines dormidas puede crear tu máquina antes de sudar. Sube de 10.000 en 10.000 y vigila la memoria del proceso.
Concurrencia en Go: primer nivel completado.
En el próximo artículo: channels y select, las tuberías por las que las goroutines se hablan.
> exit