En esta página
Channels y select
Channels: comunicación segura entre goroutines
La filosofía de Go sobre concurrencia está resumida en una frase de Rob Pike: "No se comuniquen compartiendo memoria; compartan memoria comunicándose." Los channels son el mecanismo para implementar esta filosofía.
Un channel es un conducto tipado a través del cual las goroutines se envían y reciben valores. Son fundamentales para la concurrencia en Go: en lugar de usar mutexes para proteger datos compartidos, pasas los datos a través de channels, eliminando la posibilidad de acceso concurrente a la misma variable.
Creando y usando channels
// Crear un canal sin buffer para tipo int
ch := make(chan int)
// Crear un canal con buffer de 5 elementos
ch2 := make(chan string, 5)
// Enviar un valor (operador <-)
ch2 <- "hola"
// Recibir un valor
msg := <-ch2
// Recibir ignorando el valor (solo sincronización)
<-ch2Canales sin buffer (unbuffered)
Un canal sin buffer es síncrono: el envío bloquea hasta que haya un receptor, y el receptor bloquea hasta que haya un emisor. Esta sincronización garantiza que ambas goroutines se "encuentran" en el punto del canal:
func main() {
ch := make(chan string)
go func() {
fmt.Println("Goroutine: preparando mensaje...")
ch <- "¡Hola desde la goroutine!" // bloquea hasta que main reciba
fmt.Println("Goroutine: mensaje enviado")
}()
fmt.Println("Main: esperando mensaje...")
msg := <-ch // bloquea hasta que la goroutine envíe
fmt.Println("Main recibió:", msg)
}
// Output (determinista):
// Main: esperando mensaje...
// Goroutine: preparando mensaje...
// Goroutine: mensaje enviado
// Main recibió: ¡Hola desde la goroutine!Los canales sin buffer se usan para sincronización más que para transferencia de datos.
Canales con buffer (buffered)
Un canal con buffer es asíncrono hasta que el buffer se llena. El envío solo bloquea cuando el buffer está lleno, y la recepción solo bloquea cuando el buffer está vacío:
ch := make(chan int, 3) // buffer de 3 slots
ch <- 1 // no bloquea — buffer[0]
ch <- 2 // no bloquea — buffer[1]
ch <- 3 // no bloquea — buffer[2]
// ch <- 4 // ¡bloquearía! buffer lleno
fmt.Println(len(ch)) // 3 — elementos en el buffer
fmt.Println(cap(ch)) // 3 — capacidad del buffer
n := <-ch // 1 — no bloquea, buffer tiene elementos
fmt.Println(n)
ch <- 4 // ahora cabe — buffer: [2, 3, 4]Canales direccionales
Cuando pasas un canal como parámetro, puedes especificar si solo se puede usar para enviar o para recibir. Esto documenta la intención y el compilador verifica el uso:
// Solo puede enviar (productor)
func productor(ch chan<- int) {
for i := 1; i <= 10; i++ {
ch <- i
}
close(ch)
// val := <-ch // error de compilación: cannot receive from send-only channel
}
// Solo puede recibir (consumidor)
func consumidor(ch <-chan int) {
for n := range ch {
fmt.Println(n)
}
// ch <- 42 // error de compilación: cannot send to receive-only channel
}
func main() {
ch := make(chan int, 5) // chan int (bidireccional)
go productor(ch) // se convierte implícitamente a chan<- int
consumidor(ch) // se convierte implícitamente a <-chan int
}Cerrar canales y range
Cerrar un canal señaliza que no se enviarán más valores. Los receptores pueden detectarlo:
// Detectar canal cerrado con "comma ok"
for {
val, ok := <-ch
if !ok {
fmt.Println("Canal cerrado")
break
}
fmt.Println(val)
}
// Forma idiomática: range se detiene automáticamente cuando se cierra el canal
for val := range ch {
fmt.Println(val)
}
// El loop termina cuando ch está cerrado y vaciadoRegla importante: Solo el productor debe cerrar el canal. Cerrar el canal desde el consumidor causa bugs. Enviar a un canal cerrado causa pánico.
El statement `select`
select permite que una goroutine espere múltiples operaciones de canal simultáneamente. Es como un switch pero para operaciones de canal:
select {
case msg1 := <-ch1:
fmt.Println("Recibido de ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Recibido de ch2:", msg2)
case ch3 <- "enviado":
fmt.Println("Enviado a ch3")
}Si múltiples casos están listos simultáneamente, select elige uno al azar.
`select` con `default`
El caso default hace que el select no bloquee:
// Non-blocking receive
select {
case val := <-ch:
fmt.Println("Recibido:", val)
default:
fmt.Println("Canal vacío — sin bloqueo")
}
// Non-blocking send
select {
case ch <- valor:
fmt.Println("Enviado")
default:
fmt.Println("Canal lleno — sin bloqueo, valor descartado")
}`select` en bucle — manejar múltiples fuentes
func multiplexar(ch1, ch2 <-chan string, done <-chan struct{}) {
for {
select {
case msg := <-ch1:
procesarMensaje("fuente1", msg)
case msg := <-ch2:
procesarMensaje("fuente2", msg)
case <-done:
fmt.Println("Señal de parada recibida")
return
}
}
}Timeouts con `time.After`
time.After(d) retorna un canal que recibe un valor después de la duración d. Es perfecto para implementar timeouts en operaciones de canal:
func buscarConTimeout(id int, timeout time.Duration) (*Resultado, error) {
resultadoCh := make(chan *Resultado, 1)
go func() {
// operación potencialmente lenta
resultado := buscarEnBaseDeDatos(id)
resultadoCh <- resultado
}()
select {
case resultado := <-resultadoCh:
return resultado, nil
case <-time.After(timeout):
return nil, fmt.Errorf("timeout después de %v buscando id=%d", timeout, id)
}
}
// Uso
resultado, err := buscarConTimeout(42, 5*time.Second)Canal como señal de terminación (`done channel`)
Un canal vacío chan struct{} se usa para señalizar terminación sin transferir datos:
func trabajador(jobs <-chan Trabajo, done <-chan struct{}) {
for {
select {
case trabajo := <-jobs:
procesarTrabajo(trabajo)
case <-done:
fmt.Println("Trabajador detenido")
return
}
}
}
func main() {
jobs := make(chan Trabajo, 10)
done := make(chan struct{})
go trabajador(jobs, done)
// ... enviar trabajos ...
// Señalizar parada
close(done) // cerrar un canal envía señal a todos los receivers
}Patrón: pipeline con channels
Un pipeline encadena etapas de procesamiento, cada una en su propia goroutine:
// Etapa 1: generar números
func generar(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// Etapa 2: cuadrados
func cuadrar(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
func main() {
// Encadenar el pipeline
naturales := generar(1, 2, 3, 4, 5)
cuadrados := cuadrar(naturales)
for n := range cuadrados {
fmt.Println(n) // 1, 4, 9, 16, 25
}
}Con channels y select dominados, en la siguiente lección exploraremos los patrones de concurrencia más avanzados: worker pools, fan-in/fan-out, y el fundamental context.Context.
Inicia sesión para guardar tu progreso