← Volver al listado de tecnologías

Goroutines y Channels

Por: Artiko
gogoroutineschannelsconcurrencia

Goroutines y Channels

Goroutines

Una goroutine es una funcion que se ejecuta concurrentemente. Son extremadamente ligeras: cada una usa apenas 2-8 KB de stack (vs ~1 MB de un thread del OS).

package main

import (
	"fmt"
	"time"
)

func saludar(nombre string) {
	fmt.Println("Hola,", nombre)
}

func main() {
	go saludar("Go")       // lanza goroutine
	go saludar("Mundo")    // lanza otra goroutine
	saludar("Main")        // ejecuta en goroutine principal

	time.Sleep(100 * time.Millisecond) // espera (temporal)
}

La palabra clave go antes de una llamada a funcion la ejecuta como goroutine.

Goroutines anonimas

func main() {
	go func() {
		fmt.Println("goroutine anonima")
	}() // los parentesis invocan la funcion inmediatamente

	go func(msg string) {
		fmt.Println(msg)
	}("con argumento")

	time.Sleep(100 * time.Millisecond)
}

Goroutines vs Threads

AspectoGoroutineThread del OS
Stack inicial2-8 KB (crece dinamicamente)~1 MB (fijo)
CreacionMicrosegundosMilisegundos
SchedulingGo runtime (cooperativo)Kernel del OS
Cantidad tipicaMiles a millonesCientos a miles
ComunicacionChannelsShared memory + locks
Costo de cambio de contextoNanosegundosMicrosegundos

Channels

Los channels son el mecanismo de comunicacion entre goroutines. Siguen la filosofia de Go:

“No comuniques compartiendp memoria; comparte memoria comunicandote.”

Crear y usar channels

package main

import "fmt"

func main() {
	// Crear un channel de strings
	ch := make(chan string)

	// Enviar desde una goroutine
	go func() {
		ch <- "mensaje desde goroutine" // enviar al channel
	}()

	// Recibir en main
	msg := <-ch // recibir del channel
	fmt.Println(msg)
}
// mensaje desde goroutine

El operador <- se usa para enviar y recibir:

ch <- valor   // enviar valor al channel
valor := <-ch // recibir valor del channel

Channels sin buffer (unbuffered)

Un channel sin buffer bloquea al emisor hasta que alguien recibe, y al receptor hasta que alguien envia:

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int) // sin buffer

	go func() {
		fmt.Println("enviando...")
		ch <- 42 // bloquea hasta que alguien reciba
		fmt.Println("enviado!")
	}()

	time.Sleep(time.Second) // simula trabajo
	fmt.Println("recibido:", <-ch)
}
// enviando...
// (pausa de 1 segundo)
// recibido: 42
// enviado!

Channels con buffer

Un channel con buffer permite enviar sin bloquear mientras haya espacio:

package main

import "fmt"

func main() {
	ch := make(chan int, 3) // buffer de 3 elementos

	ch <- 1 // no bloquea
	ch <- 2 // no bloquea
	ch <- 3 // no bloquea
	// ch <- 4 // BLOQUEARIA: buffer lleno

	fmt.Println(<-ch) // 1
	fmt.Println(<-ch) // 2
	fmt.Println(<-ch) // 3
}
TipoSintaxisBloqueo al enviarBloqueo al recibir
Sin buffermake(chan T)Hasta que alguien recibaHasta que alguien envie
Con buffermake(chan T, n)Cuando el buffer esta llenoCuando el buffer esta vacio

Cerrar channels

Usa close() para indicar que no se enviaran mas valores:

package main

import "fmt"

func generarNumeros(ch chan int) {
	for i := 0; i < 5; i++ {
		ch <- i
	}
	close(ch) // indica que no hay mas valores
}

func main() {
	ch := make(chan int)
	go generarNumeros(ch)

	// range itera hasta que el channel se cierre
	for num := range ch {
		fmt.Println(num)
	}
}
// 0
// 1
// 2
// 3
// 4

Detectar channel cerrado

valor, ok := <-ch
if !ok {
	fmt.Println("channel cerrado")
}

Reglas de channels cerrados:

OperacionChannel abiertoChannel cerrado
Enviar ch <- vOK (o bloquea)panic
Recibir <-chOK (o bloquea)Retorna zero value + false
Cerrar close(ch)OKpanic

Solo el emisor debe cerrar el channel. Nunca el receptor.

Patron done channel

En vez de time.Sleep, usa un channel para sincronizar goroutines:

package main

import "fmt"

func trabajar(done chan bool) {
	fmt.Println("trabajando...")
	// simula trabajo pesado
	resultado := 0
	for i := 0; i < 1000000; i++ {
		resultado += i
	}
	fmt.Println("resultado:", resultado)
	done <- true // senaliza que termino
}

func main() {
	done := make(chan bool)
	go trabajar(done)
	<-done // espera hasta que la goroutine termine
	fmt.Println("programa terminado")
}

Con struct{} para senales puras (cero memoria):

done := make(chan struct{})
go func() {
	// trabajo...
	close(done) // mas idiomatico que enviar un valor
}()
<-done

Direccionalidad de channels

Puedes restringir channels a solo enviar o solo recibir en las firmas de funciones:

package main

import "fmt"

// solo puede enviar a este channel
func productor(ch chan<- int) {
	for i := 0; i < 5; i++ {
		ch <- i
	}
	close(ch)
}

// solo puede recibir de este channel
func consumidor(ch <-chan int) {
	for v := range ch {
		fmt.Println("recibido:", v)
	}
}

func main() {
	ch := make(chan int)
	go productor(ch)
	consumidor(ch)
}
TipoSintaxisOperaciones permitidas
Bidireccionalchan TEnviar y recibir
Solo enviochan<- TSolo enviar
Solo recepcion<-chan TSolo recibir

Un chan T se convierte implicitamente a chan<- T o <-chan T. La restriccion es solo en la firma, no en la creacion.

Deadlocks comunes

Enviar sin receptor

func main() {
	ch := make(chan int)
	ch <- 42 // DEADLOCK: nadie recibe
	fmt.Println(<-ch)
}
// fatal error: all goroutines are asleep - deadlock!

Recibir sin emisor

func main() {
	ch := make(chan int)
	fmt.Println(<-ch) // DEADLOCK: nadie envia
}

Channel sin cerrar con range

func main() {
	ch := make(chan int)
	go func() {
		ch <- 1
		ch <- 2
		// olvido cerrar el channel
	}()
	for v := range ch { // DEADLOCK: espera mas valores
		fmt.Println(v)
	}
}

Como evitar deadlocks

  1. Cada send debe tener un receive correspondiente
  2. Cierra los channels cuando termines de enviar
  3. Usa channels con buffer cuando el emisor no debe esperar
  4. El Go runtime detecta deadlocks y termina el programa con error

Ejemplo completo: pipeline simple

package main

import "fmt"

func generar(nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		for _, n := range nums {
			out <- n
		}
		close(out)
	}()
	return out
}

func duplicar(in <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		for n := range in {
			out <- n * 2
		}
		close(out)
	}()
	return out
}

func main() {
	numeros := generar(1, 2, 3, 4, 5)
	dobles := duplicar(numeros)

	for resultado := range dobles {
		fmt.Println(resultado)
	}
}
// 2
// 4
// 6
// 8
// 10

Resumen