Volver al blog

La palabra clave chan en Go: channels, direcciones y deadlocks

Cómo funciona la palabra clave chan en Go: channels con y sin buffer, tipos solo de envío y solo de recepción, cierre, deadlocks, fugas de goroutines, select y pools de workers.

La palabra clave chan en Go: channels, direcciones y deadlocks

La palabra clave chan declara un tipo de channel. Un channel es un conducto con tipo que usan las goroutines para pasarse valores, y cada envío y cada recepción también sincroniza a las dos goroutines implicadas. chan Job transporta valores Job en ambas direcciones, chan<- Job solo puede enviar y <-chan Job solo puede recibir. Un channel tiene que crearse con make antes de usarlo, porque el valor cero de un tipo channel es nil (especificación de Go).

Resumen rápido

  • make(chan Job) crea un channel sin buffer. Un envío se bloquea hasta que otra goroutine recibe, así que cada envío es una entrega en mano.
  • make(chan Job, 100) crea un channel con buffer. Los envíos solo se bloquean cuando el buffer está lleno, y las recepciones solo se bloquean cuando está vacío.
  • chan<- Job y <-chan Job en la firma de una función la limitan a enviar o a recibir. El compilador rechaza cualquier otra cosa.
  • Quien envía cierra el channel, nunca quien recibe. for job := range jobs termina tras un cierre, y v, ok := <-ch devuelve ok == false cuando el channel está cerrado y vacío.
  • Enviar a un channel cerrado provoca un panic, y cerrar un channel dos veces también. Todas las operaciones sobre un channel nil salvo close se bloquean para siempre.
  • fatal error: all goroutines are asleep - deadlock! significa que todas las goroutines están bloqueadas. Una fuga es la versión silenciosa, donde solo algunas goroutines se quedan atascadas. Go 1.27 las encuentra con el perfil goroutineleak.
  • select espera sobre varias operaciones de channel a la vez. Te da timeouts, cancelación y envíos sin bloqueo.

¿Cómo se declara y se crea un channel en Go?

Escribe chan seguido del tipo de los elementos. Una variable declarada así es nil hasta que le asignas un channel creado con make:

example.gogo
package main

import "fmt"

type Job struct {
	ID  int
	URL string
}

func main() {
	var queue chan Job
	fmt.Println(queue == nil, len(queue), cap(queue))

	queue = make(chan Job, 100)
	queue <- Job{ID: 1, URL: "https://api.example.com/webhooks/stripe"}
	queue <- Job{ID: 2, URL: "https://api.example.com/webhooks/github"}
	fmt.Println(len(queue), cap(queue))

	job := <-queue
	fmt.Println(job.ID, job.URL)
}
example.texttext
true 0 0
2 100
1 https://api.example.com/webhooks/stripe

ch <- v envía y <-ch recibe. La flecha siempre apunta en la dirección en la que se mueven los datos. cap devuelve el tamaño de buffer que le pasaste a make, y len devuelve cuántos valores hay en el buffer en ese momento. Los valores salen en el mismo orden en que entraron.

Un channel es una referencia a una estructura del runtime, como un map. Pasarlo a una función o guardarlo en un struct copia la referencia, así que todas las copias hablan con el mismo channel. La palabra clave var en Go y var vs make tratan los channels, los maps y los slices como los tres tipos que normalmente se crean con make.

¿Qué diferencia hay entre los channels con buffer y sin buffer?

Un channel sin buffer no tiene almacenamiento. Un envío espera hasta que un receptor toma el valor, y una recepción espera hasta que un emisor ofrece uno. Los dos lados coinciden en el mismo instante. La especificación de Go lo expresa así: «la comunicación solo tiene éxito cuando tanto el emisor como el receptor están listos».

Aquí un handler de subidas le pasa un nombre de archivo a un escáner de virus que tarda 100ms en arrancar:

example.gogo
func main() {
	uploads := make(chan string)

	go func() {
		time.Sleep(100 * time.Millisecond)
		fmt.Println("scanner: ready")
		for name := range uploads {
			_ = name // virus-scan the file
		}
	}()

	start := time.Now()
	uploads <- "invoice-1001.pdf"
	fmt.Println("handler: send returned after", time.Since(start).Round(10*time.Millisecond))
}
example.texttext
scanner: ready
handler: send returned after 100ms

El envío no pudo completarse hasta que el escáner estuvo listo para recibir. Cuando el envío retorna, el handler sabe que el escáner tiene el valor. Esa garantía es útil por sí misma, y el modelo de memoria de Go la formula así: una recepción de un channel sin buffer está sincronizada antes de que se complete el envío correspondiente.

Un channel con buffer guarda hasta cap valores. Con make(chan string, 10), el envío del handler habría retornado al instante, y el escáner habría recogido el archivo 100ms después. El emisor solo se bloquea cuando los 10 huecos están llenos.

¿Qué tamaño de buffer debe tener un channel en Go?

Empieza con cero o uno, y elige un número mayor solo cuando puedas decir para qué sirve. Un buffer no hace más rápido a un consumidor lento. Si los productores son siempre más rápidos que los consumidores, un buffer de 1.000 se llena y luego se comporta como un channel sin buffer, con 1.000 valores de latencia extra por delante.

Los buffers ayudan en tres situaciones:

  • Una goroutine que produce un solo resultado. make(chan Result, 1) le permite enviar y terminar aunque nadie reciba, y así se arregla la fuga que aparece más abajo.
  • Ráfagas. Un exportador de métricas que recibe 50 muestras de golpe y las envía en lotes una vez por segundo puede usar un buffer del tamaño de una ráfaga normal.
  • Limitar la concurrencia. Un channel con buffer de capacidad N funciona como un semáforo que deja ejecutarse a N goroutines a la vez. La sección de patrones incluye un ejemplo.

¿Qué significan chan<- y <-chan en Go?

Son tipos de channel con dirección. chan<- Job es solo de envío y <-chan Job es solo de recepción. Un chan Job bidireccional se convierte automáticamente en cualquiera de los dos, así que creas el channel una vez y le das a cada función solo la dirección que necesita.

Un pipeline de webhooks con un productor y una etapa de entrega queda así:

example.gogo
type Result struct {
	JobID  int
	Status int
}

func produce(ids []int, out chan<- Job) {
	for _, id := range ids {
		out <- Job{ID: id, URL: fmt.Sprintf("https://hooks.example.com/%d", id)}
	}
	close(out)
}

func deliver(in <-chan Job, results chan<- Result) {
	for job := range in {
		results <- Result{JobID: job.ID, Status: 200}
	}
	close(results)
}

func main() {
	jobs := make(chan Job)
	results := make(chan Result)

	go produce([]int{101, 102, 103}, jobs)
	go deliver(jobs, results)

	for r := range results {
		fmt.Printf("job %d: %d\n", r.JobID, r.Status)
	}
}
example.texttext
job 101: 200
job 102: 200
job 103: 200

Las firmas documentan quién es dueño de cada extremo. Al leer deliver, sabes que nunca escribe en in y nunca lo cierra. El compilador lo hace cumplir. Si deliver intenta enviar a su entrada o cerrarla:

example.gogo
package main

type Job struct{ ID int }

func deliver(in <-chan Job) {
	in <- Job{ID: 1}
	close(in)
}
example.texttext
./main.go:6:2: invalid operation: cannot send to receive-only channel <-chan Job in (variable of type <-chan Job)
./main.go:7:8: invalid operation: cannot close receive-only channel in (variable of type <-chan Job)

Cerrar cuenta como una operación del lado del envío, así que solo se puede cerrar un chan<- o un channel bidireccional. Eso convierte la regla habitual de propiedad, «cierra quien envía», en algo que el sistema de tipos comprueba por ti.

¿Cómo se cierra un channel en Go?

Llama a close(ch). Cerrar indica a los receptores que no van a llegar más valores. No descarta los valores que ya están en el buffer. Los receptores reciben esos primero, y a partir de ahí cada recepción retorna de inmediato con el valor cero:

example.gogo
func main() {
	events := make(chan string, 2)
	events <- "user.created"
	events <- "user.deleted"
	close(events)

	for i := 0; i < 3; i++ {
		ev, ok := <-events
		fmt.Printf("%q %v\n", ev, ok)
	}
}
example.texttext
"user.created" true
"user.deleted" true
"" false

El segundo valor, ok, es true para un valor real y false cuando el channel está cerrado y vacío. Un simple ev := <-events no puede distinguir un string vacío que se envió del valor cero de un channel cerrado, así que usa la forma con dos valores siempre que el channel pueda estar cerrado.

for v := range ch hace esa misma comprobación por ti. Recibe hasta que el channel está cerrado y vacío, y luego sale del bucle. Así es como deliver, más arriba, sabe cuándo parar, y por eso produce tiene que cerrar jobs cuando se queda sin IDs. Sin el cierre, deliver esperaría para siempre un cuarto trabajo.

¿Quién debe cerrar un channel en Go?

La goroutine que envía. El receptor no puede saber si viene otro valor en camino, y cerrar desde el lado del receptor lleva directamente a los panics de abajo. Cuando varias goroutines envían a un mismo channel, ninguna puede cerrarlo por su cuenta. Una goroutine aparte espera a todas con un sync.WaitGroup y cierra el channel después. El pool de workers de más abajo muestra esa estructura.

Tres errores provocan un panic en tiempo de ejecución:

example.texttext
panic: send on closed channel
panic: close of closed channel
panic: close of nil channel

No hace falta cerrar todos los channels. Un channel que nadie referencia se recolecta como basura tanto si se cerró como si no. Cierra un channel cuando los receptores necesiten saber que el flujo ha terminado, como en un bucle range o una señal de apagado.

¿Qué pasa si envías a un channel nil o cerrado?

Aquí tienes todas las combinaciones en un solo sitio:

OperaciónChannel nilChannel abiertoChannel cerrado
Envío ch <- vSe bloquea para siempreSe bloquea hasta que un receptor lo toma o se libera espacio en el bufferPanic
Recepción <-chSe bloquea para siempreDevuelve un valor del buffer, o se bloquea hasta que llega unoDevuelve los valores del buffer y luego el valor cero con ok == false
close(ch)PanicFuncionaPanic

Un caso de select sobre un channel nil nunca se elige, y el ejemplo de fusión de más abajo se apoya en eso. Un channel cerrado retorna al instante para todos los receptores, lo que convierte a close en un broadcast. Todas las goroutines que reciben del channel se despiertan a la vez.

¿Por qué Go dice "all goroutines are asleep - deadlock!"?

Porque el runtime detectó que todas las goroutines del programa están bloqueadas y ninguna puede despertar nunca. La versión más pequeña es un envío a un channel sin buffer sin ningún receptor:

example.gogo
func main() {
	uploads := make(chan string)
	uploads <- "invoice-1001.pdf"
	fmt.Println("queued")
}
example.texttext
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()
	/app/main.go:7 +0x34

main es la única goroutine, y espera en un envío que nadie va a recibir. La traza indica la operación bloqueada (chan send) y la línea. Las soluciones son recibir en otra goroutine o darle al channel un buffer si solo necesitas aparcar un valor. Errores comunes en Go lo incluye como el décimo error.

El detector solo salta cuando todas las goroutines están atascadas. Un servidor web siempre tiene una goroutine esperando en el poller de red la siguiente conexión, y el detector no la cuenta como atascada, así que un handler bloqueado en un channel nunca lo dispara. El handler simplemente se detiene, y su goroutine se queda en memoria hasta que el proceso termina. Esto se llama fuga de goroutines.

¿Cómo se encuentran las fugas de goroutines en Go?

Una fuga habitual tiene este aspecto. Una página de checkout pide presupuesto a tres empresas de transporte y se queda con la respuesta más rápida:

example.gogo
func fastestQuote(ctx context.Context) (Quote, error) {
	ch := make(chan Quote)
	go func() { ch <- fetchQuote("ups", 50*time.Millisecond) }()
	go func() { ch <- fetchQuote("dhl", 80*time.Millisecond) }()
	go func() { ch <- fetchQuote("fedex", 300*time.Millisecond) }()

	select {
	case q := <-ch:
		return q, nil
	case <-ctx.Done():
		return Quote{}, ctx.Err()
	}
}

La función recibe una vez y retorna. Las dos goroutines más lentas intentan entonces enviar a un channel sin buffer que nadie va a leer jamás. Se quedan bloqueadas mientras el proceso siga en marcha. Tras 100 checkouts, hay 200 goroutines atascadas.

Desde Go 1.27, el perfil goroutineleak está disponible de forma general, tras pasar una versión como experimento en Go 1.26 (notas de la versión Go 1.27). El recolector de basura busca goroutines bloqueadas en un channel o un lock que ya no es alcanzable desde ninguna goroutine ejecutable, y las informa agrupadas por pila:

example.gogo
pprof.Lookup("goroutineleak").WriteTo(os.Stdout, 1)
example.texttext
goroutineleak profile: total 200
100 @ 0x1044f99a8 0x104492854 0x104492468 0x104550560 0x1044ffc74
#	0x10455055f	main.fastestQuote.func3+0x4f	/app/main.go:27

100 @ 0x1044f99a8 0x104492854 0x104492468 0x1045505d0 0x1044ffc74
#	0x1045505cf	main.fastestQuote.func2+0x4f	/app/main.go:26

El perfil indica la línea exacta de las goroutines de DHL y FedEx. Un servidor que importa net/http/pprof expone los mismos datos en /debug/pprof/goroutineleak. Novedades de Go 1.27 trata el perfil con más detalle.

La solución es un buffer con sitio para todos los emisores:

example.gogo
ch := make(chan Quote, 3)

Ahora cada goroutine puede enviar y terminar, la lea alguien o no. Con ese cambio, el mismo programa informa goroutineleak profile: total 0. En general, cada goroutine que inicias necesita alguna forma de terminar. Normalmente es un receptor que siempre lee, un buffer lo bastante grande para todos los envíos o un select sobre ctx.Done().

¿Cómo funciona select con channels en Go?

select espera sobre varias operaciones de channel y ejecuta la primera que puede avanzar. Si varias están listas a la vez, elige una al azar, así que ningún caso puede dejar sin turno a los demás (especificación de Go).

¿Cómo se añade un timeout a la recepción de un channel?

Pon la recepción y un channel de timeout en el mismo select. Con un context.Context, el channel de timeout es ctx.Done():

example.gogo
func lookupPrice(ctx context.Context, sku string) (int, error) {
	result := make(chan int, 1)
	go func() {
		result <- slowPriceService(sku)
	}()

	select {
	case cents := <-result:
		return cents, nil
	case <-ctx.Done():
		return 0, fmt.Errorf("price for %s: %w", sku, ctx.Err())
	}
}

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
	defer cancel()
	fmt.Println(lookupPrice(ctx, "SKU-1001"))

	fmt.Println(lookupPrice(context.Background(), "SKU-1001"))
}
example.texttext
0 price for SKU-1001: context deadline exceeded
4999 <nil>

slowPriceService tarda 300ms, así que la primera llamada se rinde a los 100ms y la segunda espera la respuesta. El buffer de 1 en result es la solución a la fuga de la sección anterior. Tras un timeout, la goroutine todavía puede enviar su respuesta tardía y terminar.

case <-time.After(2 * time.Second): también funciona cuando no hay context. Desde Go 1.23, un timer que nadie referencia se puede recolectar antes de que se dispare, así que time.After dentro de un bucle ya no acumula timers (notas de la versión Go 1.23). Go 1.27 eliminó el ajuste GODEBUG asynctimerchan que restauraba el comportamiento antiguo. En código que atiende peticiones, es preferible el context, porque también transmite la cancelación del cliente.

¿Cómo se hace un envío o una recepción sin bloqueo?

Añade un caso default. select ejecuta default cuando ningún otro caso está listo, así que la operación nunca espera. Un exportador de métricas que nunca debe ralentizar el camino de las peticiones lo usa para descartar muestras cuando su cola está llena:

example.gogo
type Exporter struct {
	queue   chan Metric
	dropped atomic.Int64
}

func (e *Exporter) Record(m Metric) {
	select {
	case e.queue <- m:
	default:
		e.dropped.Add(1)
	}
}

func main() {
	e := &Exporter{queue: make(chan Metric, 3)}
	for i := range 5 {
		e.Record(Metric{Name: "http_requests_total", Value: float64(i)})
	}
	fmt.Println("queued:", len(e.queue), "dropped:", e.dropped.Load())
}
example.texttext
queued: 3 dropped: 2

Record se ejecuta en muchos handlers de peticiones a la vez, así que el contador de descartes es un atomic.Int64 y no un int normal. Lleva la cuenta de los descartes, porque un default silencioso oculta el momento en que el consumidor se queda atrás.

¿Por qué asignar nil a un channel en un select?

Para desactivar un caso. Una recepción de un channel nil se bloquea para siempre, así que un caso de select sobre un channel nil nunca se puede elegir. Cuando fusionas dos flujos, asigna nil a cada uno en cuanto se cierre, y para cuando ambos lo estén:

example.gogo
func merge(a, b <-chan string) []string {
	var out []string
	for a != nil || b != nil {
		select {
		case v, ok := <-a:
			if !ok {
				a = nil
				continue
			}
			out = append(out, v)
		case v, ok := <-b:
			if !ok {
				b = nil
				continue
			}
			out = append(out, v)
		}
	}
	return out
}

Sin la asignación a nil, un channel cerrado siempre está listo, y el bucle giraría sobre valores cero consumiendo toda la CPU.

¿Cuáles son los patrones de channels más comunes en Go?

Cuatro patrones cubren la mayor parte del código con channels en servicios en producción.

Pool de workers

Un número fijo de goroutines lee de un mismo channel de trabajos. Este entrega webhooks con tres workers:

example.gogo
func main() {
	hooks := make(chan Webhook)
	deliveries := make(chan Delivery)

	var wg sync.WaitGroup
	for worker := range 3 {
		wg.Go(func() {
			for h := range hooks {
				deliveries <- Delivery{WebhookID: h.ID, Worker: worker, Status: send(h)}
			}
		})
	}

	go func() {
		for id := 1; id <= 9; id++ {
			hooks <- Webhook{ID: id, URL: fmt.Sprintf("https://partner.example.com/hooks/%d", id)}
		}
		close(hooks)
	}()

	go func() {
		wg.Wait()
		close(deliveries)
	}()

	ok := 0
	for d := range deliveries {
		if d.Status == 200 {
			ok++
		}
	}
	fmt.Println("delivered:", ok)
}
example.texttext
delivered: 9

El código es correcto gracias a cuatro detalles. El productor cierra hooks, así que el bucle range de cada worker termina. wg.Go, añadido en Go 1.25, inicia cada worker y lo registra en una sola llamada. Una goroutine aparte espera a todos los workers antes de cerrar deliveries, porque ningún worker sabe por sí solo cuándo han terminado los demás. Las closures capturan worker sin problema desde que Go 1.22 dio a cada iteración del bucle su propia variable (consulta cómo funcionan las closures).

Fan-out y fan-in

El pool de workers es fan-out: un channel alimenta a muchas goroutines. El channel deliveries es fan-in: muchas goroutines escriben en un channel que lee un único bucle. La función merge de más arriba es fan-in para un número fijo de channels de entrada. El artículo sobre pipelines del blog de Go construye cadenas más largas con esas mismas dos piezas.

Semáforo

Un channel con buffer de structs vacíos limita cuántas goroutines hacen algo a la vez. Un servicio de imágenes que no debe redimensionar más de tres imágenes en paralelo:

example.gogo
sem := make(chan struct{}, 3)
var wg sync.WaitGroup
for _, img := range images {
	wg.Go(func() {
		sem <- struct{}{}
		defer func() { <-sem }()
		resizeImage(img)
	})
}
wg.Wait()

La cuarta goroutine se bloquea en sem <- struct{}{} hasta que una de las tres primeras libera su hueco. El modelo de memoria lo garantiza para un channel de capacidad C: la k-ésima recepción ocurre antes de que se complete el envío número k+C (modelo de memoria de Go). Cuando las goroutines también devuelven errores, errgroup.Group con SetLimit de golang.org/x/sync hace lo mismo y recoge el primer error.

Señal de finalización con chan struct{}

Un channel que solo se cierra, sin que nunca se envíe nada por él, transmite una señal a todos los receptores. struct{} es el tipo de elemento porque ocupa cero bytes y deja claro que no viajan datos (consulta la palabra clave struct en Go):

example.gogo
func flushLoop(done <-chan struct{}, flushed chan<- int) {
	ticker := time.NewTicker(50 * time.Millisecond)
	defer ticker.Stop()
	n := 0
	for {
		select {
		case <-ticker.C:
			n++
		case <-done:
			flushed <- n
			return
		}
	}
}

func main() {
	done := make(chan struct{})
	flushed := make(chan int)
	go flushLoop(done, flushed)

	time.Sleep(175 * time.Millisecond)
	close(done)
	fmt.Println("flushes before shutdown:", <-flushed)
}
example.texttext
flushes before shutdown: 3

context.Context se basa en la misma idea. ctx.Done() devuelve un <-chan struct{} que se cierra al cancelar, así que funciona en cualquier select igual que done en el ejemplo anterior.

¿Channel o mutex en Go?

Usa un channel cuando pasas la propiedad de unos datos o coordinas goroutines. Usa un sync.Mutex cuando varias goroutines actualizan en su sitio un mismo estado. El proverbio de Go «no comuniques compartiendo memoria, comparte memoria comunicando» (Go Proverbs) describe el primer caso, y la página de la wiki de Go Use a sync.Mutex or a channel? dice claramente que el proverbio no descarta los mutex.

Un contador de peticiones por ruta es estado, no un flujo de valores:

example.gogo
type Stats struct {
	mu     sync.Mutex
	counts map[string]int
}

func (s *Stats) Inc(route string) {
	s.mu.Lock()
	defer s.mu.Unlock()
	s.counts[route]++
}

La versión con channels necesita una goroutine que sea dueña del map, un channel de peticiones y un channel de respuesta para las lecturas. Es más código, y cada incremento tiene que esperar a que otra goroutine lo procese. Con una cola de trabajos pasa lo contrario. Un slice protegido por un mutex necesita un sync.Cond o sondeo para que los workers esperen trabajo, mientras que un channel los bloquea y los despierta sin coste adicional.

Usa un channel paraUsa un mutex para
Pasar un trabajo o un resultado a otra goroutineContadores, cachés y maps que se actualizan en su sitio
Señalar apagado, cancelación o finalizaciónProteger unos pocos campos dentro de un struct
Limitar la concurrencia o construir pipelinesSecciones críticas cortas en caminos calientes

Prueba ambos con go test -race. Detecta un map compartido sin lock, y también un emisor que sigue modificando un struct después de enviar un puntero a él mientras el receptor lo lee.

Dónde encaja LevelUpGo

LevelUpGo enseña Go con ejercicios que ejecutan código Go real en el navegador. Go Concurrency Fundamentals trata las goroutines, los channels con y sin buffer, las direcciones de los channels, select, context y el detector de carreras, con ejercicios que se quedan en deadlock hasta que los arreglas. Go Design Patterns construye como ejercicios completos los patrones de este artículo: el pool de workers, fan-out y fan-in, y el apagado ordenado. El Training Ground tiene ejercicios cortos e independientes de concurrencia con código que se bloquea, tiene fugas o pierde trabajo. Para las otras 24 palabras reservadas, consulta Palabras clave de Go: las 25 explicadas.

Preguntas frecuentes

¿Es chan una palabra clave en Go?

Sí. chan es una de las 25 palabras clave reservadas de Go, así que no puedes usarla como nombre de variable ni de función. Aparece en tipos de channel como chan Job, chan<- Job y <-chan Job. El operador <- no es una palabra clave, y make tampoco, porque es una función predeclarada.

¿Hay que cerrar siempre un channel en Go?

No. Cierra un channel solo cuando los receptores necesiten saber que no van a llegar más valores, por ejemplo para terminar un bucle for range o para anunciar un apagado. Un channel sin cerrar al que nada referencia se recolecta como cualquier otro valor.

¿Qué diferencia hay entre un channel con buffer y uno sin buffer?

Un channel sin buffer, make(chan T), hace que el emisor espere hasta que un receptor tome el valor, así que las dos goroutines coinciden. Un channel con buffer, make(chan T, n), deja que hasta n valores esperen en el channel, y el emisor solo se bloquea cuando el buffer está lleno.

¿Qué pasa al leer de un channel cerrado en Go?

Primero recibes los valores que queden en el buffer. Después, cada recepción retorna de inmediato con el valor cero del tipo de elemento. La forma con dos valores v, ok := <-ch pone ok a false para esos valores cero, así que puedes distinguirlos de los reales.

¿Cómo se comprueba si un channel está cerrado en Go?

Recibe de él con v, ok := <-ch y comprueba ok. No existe ninguna función que indique si un channel está cerrado sin recibir, y len(ch) solo cuenta los valores del buffer. El código que necesita esa comprobación suele tener más de una goroutine cerrando el channel, y ese es el diseño que hay que corregir.

¿Se puede recorrer un channel con range en Go?

Sí. for v := range ch recibe valores hasta que el channel está cerrado y vacío, y luego sale del bucle. Si nadie cierra el channel, el bucle espera para siempre, así que el emisor tiene que llamar a close cuando termine.

Fuentes

Escribe Go como un ingeniero sénior

Lecciones interactivas en tu navegador. Las primeras son gratis.

Prueba una lección gratisO crea una cuenta gratuita