Volver al blog

La palabra clave for en Go: bucles, range, variables de bucle y etiquetas

Cómo funciona la palabra clave for en Go: las tres formas del bucle, for range sobre slices, maps, strings, channels, enteros e iteradores, las variables de bucle de Go 1.22, break y continue con etiquetas, y los errores habituales con bucles.

La palabra clave for en Go: bucles, range, variables de bucle y etiquetas

for es la única palabra clave de bucle en Go. Tiene tres formas: el bucle de tres cláusulas for i := 0; i < n; i++, el bucle solo con condición for cond, que es el bucle while de Go, y el bucle infinito for { ... }. Si añades range, la misma palabra clave recorre slices, arrays, maps, strings, channels, enteros y funciones iteradoras. Go no tiene las palabras clave while, do, foreach ni loop (especificación de Go, For statements).

Resumen rápido

  • for init; cond; post { ... } es el bucle con contador. Sin paréntesis alrededor de las cláusulas, y las llaves son obligatorias.
  • for cond { ... } es un bucle while, y for { ... } se repite hasta un break, un return o un panic.
  • for i, v := range x recorre un slice, array, map, string, channel, entero (Go 1.22) o función iteradora (Go 1.23). El orden de un map es aleatorio.
  • Desde Go 1.22, cada iteración recibe una variable de bucle nueva si go.mod declara go 1.22 o posterior. Las goroutines lanzadas en un bucle ya no ven todas el último valor.
  • El valor de range es una copia. Asigna a items[i] para modificar el slice.
  • Un break sin etiqueta dentro de un select o un switch solo sale de esa sentencia. Usa una etiqueta para salir del bucle.

¿Qué formas tiene un bucle for en Go?

La especificación define una única sentencia for con tres formas. Solo se diferencian en qué cláusulas escribes.

El bucle de tres cláusulas

La sentencia de inicialización se ejecuta una vez, la condición se comprueba antes de cada iteración y la sentencia posterior se ejecuta después de cada iteración. Este bucle reintenta un health check inestable con backoff exponencial:

example.gogo
package main

import (
	"errors"
	"fmt"
	"time"
)

var errUnavailable = errors.New("503 service unavailable")

func fetchStatus(attempt int) error {
	if attempt < 3 {
		return errUnavailable
	}
	return nil
}

func main() {
	backoff := 100 * time.Millisecond
	for attempt := 1; attempt <= 5; attempt++ {
		err := fetchStatus(attempt)
		if err == nil {
			fmt.Printf("attempt %d: ok\n", attempt)
			break
		}
		fmt.Printf("attempt %d: %v, retrying in %v\n", attempt, err, backoff)
		time.Sleep(backoff)
		backoff *= 2
	}
}
example.texttext
attempt 1: 503 service unavailable, retrying in 100ms
attempt 2: 503 service unavailable, retrying in 200ms
attempt 3: ok

attempt solo existe dentro del bucle. Cualquiera de las tres cláusulas se puede omitir. for ; attempt <= 5; { es válido, y gofmt lo reescribe como for attempt <= 5 {.

La costumbre de C de envolver las cláusulas entre paréntesis no compila:

example.gogo
	for (i := 0; i < 3; i++) {
example.texttext
./main.go:6:9: syntax error: unexpected :=, expected )

El bucle solo con condición (while)

Quita las sentencias de inicialización y posterior, y for se convierte en un bucle while. Este vacía una cola de trabajos hasta que no queda ninguno:

example.gogo
package main

import "fmt"

func main() {
	queue := []string{"resize:42.png", "resize:43.png", "thumbnail:44.png"}

	for len(queue) > 0 {
		job := queue[0]
		queue = queue[1:]
		fmt.Println("processing", job)
	}
	fmt.Println("queue empty")
}
example.texttext
processing resize:42.png
processing resize:43.png
processing thumbnail:44.png
queue empty

Si escribes while en su lugar, obtienes un error de análisis, porque while es un identificador normal en Go:

example.texttext
./main.go:7:8: syntax error: unexpected name pending at end of statement

El compilador lee while como una expresión, encuentra otro nombre detrás y se rinde. El mensaje no dice nada de bucles, y por eso confunde a quien viene de otros lenguajes.

El bucle infinito

Sin ninguna cláusula, for { ... } se ejecuta hasta que algo en su interior lo abandona. Los servidores usan esta forma para los bucles de accept y los pollers:

example.gogo
	for {
		conn, err := ln.Accept()
		if err != nil {
			if errors.Is(err, net.ErrClosed) {
				return
			}
			log.Printf("accept: %v", err)
			continue
		}
		go handle(conn)
	}

El bucle termina cuando se cierra el listener y Accept devuelve net.ErrClosed. for {} sustituye a while (true) y a for (;;), y el compilador sabe que nunca termina por sí solo. Una función que acaba en for {} sin ningún break no necesita un return final.

¿Cómo funciona for range?

range convierte un bucle for en un recorrido sobre un valor. Lo que obtienes depende del tipo:

Recorres con rangePrimer valorSegundo valor
Slice o arrayíndicecopia del elemento
Stringposición en bytesrune (punto de código Unicode)
Mapclavevalor
Channelvalor recibidoninguno
Entero n (Go 1.22)de 0 a n-1ninguno
Función iteradora (Go 1.23)lo que produzcalo que produzca

Puedes omitir la segunda variable (for i := range items) o ignorar la primera con _ (for _, item := range items). for range n sin variables ejecuta un bloque n veces.

example.gogo
package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	endpoints := []string{"/health", "/orders", "/users"}
	for i, path := range endpoints {
		fmt.Println(i, path)
	}

	statusCounts := map[int]int{200: 812, 404: 17, 500: 3}
	for _, code := range slices.Sorted(maps.Keys(statusCounts)) {
		fmt.Printf("%d: %d\n", code, statusCounts[code])
	}

	for offset, r := range "café" {
		fmt.Printf("%d:%c ", offset, r)
	}
	fmt.Println()

	for attempt := range 3 {
		fmt.Print(attempt, " ")
	}
	fmt.Println()

	results := make(chan string, 2)
	results <- "job 1 done"
	results <- "job 2 done"
	close(results)
	for msg := range results {
		fmt.Println(msg)
	}
}
example.texttext
0 /health
1 /orders
2 /users
200: 812
404: 17
500: 3
0:c 1:a 2:f 3:é 
0 1 2 
job 1 done
job 2 done

El bucle del map es el que hay que vigilar. El runtime aleatoriza a propósito el orden de iteración de los maps para que el código no pueda depender de él. Para imprimir los códigos de estado en orden, maps.Keys devuelve un iterador sobre las claves y slices.Sorted las recoge y las ordena. La palabra clave map en Go trata la iteración de maps con más detalle.

El bucle del string produce runes, y las posiciones son posiciones en bytes, así que é empieza en la posición 3 y ocupa dos bytes. Usa for i := 0; i < len(s); i++ con s[i] cuando quieras los bytes tal cual.

El bucle del channel se detiene cuando el channel se cierra. Si nadie lo cierra, el bucle se bloquea para siempre en cuanto el buffer se vacía. La palabra clave chan en Go explica quién debe cerrar un channel.

Las funciones iteradoras (Go 1.23) permiten usar con range cualquier función con la firma func(yield func(K, V) bool). La biblioteca estándar las devuelve desde maps.Keys, maps.All, slices.All, slices.Backward y strings.Lines, y puedes escribir las tuyas para resultados paginados de una API o filas de una base de datos. range tiene más reglas de las que caben aquí. La sección de range de la guía de palabras clave de Go es la versión corta.

¿Qué cambió en las variables de bucle en Go 1.22?

Antes de Go 1.22, un bucle for declaraba sus variables una sola vez y las reutilizaba en cada iteración. Una closure o goroutine que capturaba la variable veía el valor que tuviera en el momento de ejecutarse, que solía ser el último. Go 1.22 lo cambió para que cada iteración reciba una variable nueva (blog de Go, Fixing For Loops in Go 1.22).

Este health checker lanza una goroutine por cada host de base de datos:

example.gogo
package main

import (
	"fmt"
	"sync"
)

func main() {
	hosts := []string{"db-1", "db-2", "db-3"}

	var wg sync.WaitGroup
	for _, host := range hosts {
		wg.Add(1)
		go func() {
			defer wg.Done()
			fmt.Println("checking", host)
		}()
	}
	wg.Wait()
}

Con go 1.21 en go.mod, tres ejecuciones imprimieron checking db-3 tres veces cada una, y go vet informó:

example.texttext
main.go:16:28: loop variable host captured by func literal

Con go 1.27 en go.mod, el mismo código comprueba los tres hosts en el orden en que terminen las goroutines, y go vet no tiene nada que decir.

El cambio depende de la línea go de go.mod, no del toolchain con el que compilas. Un compilador de Go 1.27 sigue compilando con la semántica antigua un módulo que declara go 1.21, así que una dependencia escrita antes de 1.22 sigue funcionando como la probaron sus autores. El cambio se aplica tanto a la forma de tres cláusulas como a range. El viejo apaño host := host dentro del bucle ya sobra. El modernizador forvar de go fix lo elimina por ti. La palabra clave func en Go trata las closures y las variables capturadas con más detalle.

¿Por qué modificar el valor de range no cambia el slice?

La segunda variable de range es una copia del elemento. En un slice de structs, cambiar un campo de la copia deja el slice intacto:

example.gogo
package main

import "fmt"

type Invoice struct {
	ID     string
	Amount int
	Paid   bool
}

func main() {
	invoices := []Invoice{
		{ID: "inv-101", Amount: 4200},
		{ID: "inv-102", Amount: 1800},
	}

	for _, inv := range invoices {
		inv.Paid = true
	}
	fmt.Println(invoices[0].Paid, invoices[1].Paid)

	for i := range invoices {
		invoices[i].Paid = true
	}
	fmt.Println(invoices[0].Paid, invoices[1].Paid)
}
example.texttext
false false
true true

El primer bucle pone Paid en una copia que se descarta al final de cada iteración. El segundo bucle accede al slice por índice y cambia el elemento real. Go 1.22 no cambió esto. La corrección de 1.22 dio a cada iteración su propia variable, y esa variable sigue siendo una copia. En un slice de punteros ([]*Invoice), la copia es un puntero y inv.Paid = true sí actualiza la factura.

Guardar un puntero a un elemento

&invoices[i] te da un puntero al array subyacente del slice. Ese puntero queda obsoleto si más adelante el slice crece por encima de su capacidad, porque append copia los elementos a un array nuevo:

example.gogo
	var large *Invoice
	for i := range invoices {
		if invoices[i].Amount > 4000 {
			large = &invoices[i]
		}
	}

	invoices = append(invoices, Invoice{"inv-103", 900})
	large.Amount = 0
	fmt.Println(invoices[0].Amount, large.Amount)
example.texttext
4200 0

large sigue apuntando al array antiguo, así que la escritura nunca llega a invoices. Guarda el índice (largeIdx = i) en lugar de un puntero cuando el slice pueda crecer.

¿Cómo funcionan break y continue en un bucle for?

break sale del for, switch o select más interno. continue salta a la siguiente iteración del for más interno. Los dos aceptan una etiqueta opcional que nombra un bucle externo.

Una etiqueta es un identificador seguido de dos puntos, colocado en la línea anterior al bucle. Esta búsqueda se detiene en el primer error de varios archivos de log:

example.gogo
package main

import "fmt"

func main() {
	logLines := [][]string{
		{"INFO start", "INFO ready"},
		{"INFO request", "ERROR db timeout", "INFO retry"},
		{"INFO shutdown"},
	}

files:
	for file, lines := range logLines {
		for _, line := range lines {
			if line[:5] == "ERROR" {
				fmt.Printf("file %d: first error %q\n", file, line)
				break files
			}
		}
	}
}
example.texttext
file 1: first error "ERROR db timeout"

Sin la etiqueta, break solo saldría del bucle interno y la búsqueda pasaría al tercer archivo. continue files se saltaría el resto del archivo actual y pasaría al siguiente.

break dentro de select

Este bug compila y parece correcto en una lectura rápida. Un worker lee trabajos hasta que se cancela su context:

example.gogo
func worker(ctx context.Context, jobs <-chan string) {
	for {
		select {
		case <-ctx.Done():
			fmt.Println("worker stopping:", ctx.Err())
			break
		case job := <-jobs:
			fmt.Println("handled", job)
		}
	}
	fmt.Println("worker flushed metrics")
}

El break sale del select, no del for. El worker imprime worker stopping y vuelve directamente al select, donde ctx.Done() sigue cerrado, así que imprime la misma línea una y otra vez en un bucle cerrado. Las métricas nunca se vuelcan. go vet informa del código muerto que queda después del bucle:

example.texttext
main.go:20:2: unreachable code

y staticcheck señala el propio break:

example.texttext
main.go:15:4: ineffective break statement. Did you mean to break out of the outer loop? (SA4011)

La solución es una etiqueta en el bucle:

example.gogo
func worker(ctx context.Context, jobs <-chan string) {
loop:
	for {
		select {
		case <-ctx.Done():
			fmt.Println("worker stopping:", ctx.Err())
			break loop
		case job := <-jobs:
			fmt.Println("handled", job)
		}
	}
	fmt.Println("worker flushed metrics")
}
example.texttext
handled email:1
handled email:2
worker stopping: context deadline exceeded
worker flushed metrics

Un return en el caso ctx.Done() también funciona cuando no hay nada que hacer después del bucle. La misma trampa se aplica a un break dentro de un switch que está dentro de un bucle.

Errores comunes con el bucle for

defer dentro de un bucle

defer se ejecuta cuando la función retorna, no cuando termina la iteración. Un bucle que abre un archivo y aplaza Close con defer mantiene todos los archivos abiertos hasta que termina la función entera:

example.gogo
	for _, p := range paths {
		f, err := os.Open(p)
		if err != nil {
			return 0, err
		}
		defer f.Close()
		// ...
	}

Con un puñado de rutas no se nota, pero con diez mil archivos de log el proceso se queda sin descriptores de archivo. Ni go vet ni staticcheck lo marcan. Mueve el cuerpo a una función para que cada defer se ejecute al final de su propia llamada, o llama a f.Close() directamente al final de la iteración. Errores comunes en Go que debes evitar trata este caso con un ejemplo completo.

Añadir a un slice mientras lo recorres con range

range evalúa su operando una sola vez, antes de la primera iteración. Hacer append dentro del bucle no hace que se ejecute más veces:

example.gogo
	urls := []string{"/", "/about"}
	for _, u := range urls {
		urls = append(urls, u+"/sitemap.xml")
	}
	fmt.Println(len(urls), urls)
example.texttext
4 [/ /about //sitemap.xml /about/sitemap.xml]

El bucle se ejecutó dos veces, una por cada elemento original. Eso lo hace seguro, pero también significa que un crawler no puede descubrir URLs nuevas de esta forma. Usa un bucle con condición sobre una cola, como el for len(queue) > 0 de arriba, cuando la lista de trabajo crece sobre la marcha.

Modificar un map durante range

Borrar la clave actual mientras recorres un map con range está permitido y es seguro. Este bucle elimina las sesiones inactivas:

example.gogo
	for id, idle := range sessions {
		if idle > 30 {
			delete(sessions, id)
		}
	}

Añadir claves durante el bucle también está permitido, pero la especificación dice que una entrada nueva «puede producirse durante la iteración o puede omitirse» ("may be produced during the iteration or may be skipped"). Recoge las claves nuevas en un map aparte y combínalas después si hay que procesar todas.

Capturar la dirección de la variable de bucle

ptrs = append(ptrs, &inv) dentro de for _, inv := range invoices te da punteros a copias, no a los elementos del slice. Antes de Go 1.22, todos los punteros apuntaban a la misma variable y mostraban la última factura. Desde Go 1.22, cada puntero apunta a su propia copia, lo cual es correcto pero sigue desconectado del slice. Usa &invoices[i] si quieres modificar los originales, y ten en cuenta el aviso sobre la capacidad de más arriba.

¿Existe un bucle while o do-while en Go?

No. Effective Go dice que el bucle for de Go «unifica for y while, y no hay do-while». La forma solo con condición cubre lo que otros lenguajes hacen con while.

Un do-while ejecuta el cuerpo una vez antes de comprobar la condición. En Go lo escribes como un bucle infinito con la comprobación al final. Las llamadas paginadas a una API son el caso típico, porque la primera página siempre se pide:

example.gogo
	cursor := ""
	for {
		page := fetchPage(cursor)
		fmt.Println("fetched", page.Items)
		cursor = page.Next
		if cursor == "" {
			break
		}
	}
example.texttext
fetched [order-1 order-2]
fetched [order-3]

Son dos líneas más de las que ocuparía do { } while (cursor != ""), y la condición de salida está al final, justo donde la buscarías.

Dónde encaja LevelUpGo

LevelUpGo enseña Go con ejercicios que ejecutan código Go real en el navegador. Go Basics tiene dos lecciones sobre for: una sobre las tres formas del bucle y otra sobre range, break y continue. Concurrency Fundamentals cubre los bucles que dan problemas en código concurrente, como lanzar goroutines en un bucle, recorrer channels con range y salir de un bucle for/select al cancelar. Para las otras 24 palabras reservadas, consulta Palabras clave de Go: las 25 explicadas.

Preguntas frecuentes

¿Tiene Go un bucle while?

No, Go no tiene la palabra clave while. Escribe for cond { ... } para un bucle while y for { ... } para un bucle infinito. Escribir while da un error de sintaxis, porque el compilador lo trata como un nombre cualquiera.

¿Tiene Go un bucle foreach?

for ... range es el foreach de Go. for _, v := range items visita cada elemento de un slice, y la misma forma funciona con arrays, maps, strings, channels, enteros y funciones iteradoras.

¿Cómo repito un bucle N veces en Go?

Desde Go 1.22, for i := range n se ejecuta con i de 0 a n-1, y for range n se ejecuta n veces sin variable. La forma de tres cláusulas for i := 0; i < n; i++ funciona en todas las versiones.

¿Cómo recorro un map en orden en Go?

Recoge y ordena las claves, y luego recórrelas con range. Desde Go 1.23, for _, k := range slices.Sorted(maps.Keys(m)) hace las dos cosas en una línea. El orden de iteración de un map es aleatorio a propósito, así que nunca dependas de él.

¿Cómo recorro un slice al revés en Go?

Usa for i := len(s) - 1; i >= 0; i-- o, desde Go 1.23, for i, v := range slices.Backward(s), que produce el índice y el valor desde el último elemento hasta el primero.

¿Por qué mi goroutine dentro de un bucle for imprime el mismo valor?

Tu módulo está en una versión de Go anterior a 1.22, en la que todas las iteraciones comparten la variable de bucle. Cambia la línea go de go.mod a go 1.22 o posterior y cada iteración tendrá su propia variable. En una versión anterior, pasa el valor como argumento (go func(h string) { ... }(host)) o cópialo con host := host dentro del bucle.

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