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, yfor { ... }se repite hasta unbreak, unreturno unpanic.for i, v := range xrecorre 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.moddeclarago 1.22o 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
breaksin etiqueta dentro de unselecto unswitchsolo 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.gogopackage 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.texttextattempt 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.gogofor (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.gogopackage 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.texttextprocessing 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.gogofor { 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 range | Primer valor | Segundo valor |
|---|---|---|
| Slice o array | índice | copia del elemento |
| String | posición en bytes | rune (punto de código Unicode) |
| Map | clave | valor |
| Channel | valor recibido | ninguno |
Entero n (Go 1.22) | de 0 a n-1 | ninguno |
| Función iteradora (Go 1.23) | lo que produzca | lo 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.gogopackage 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.texttext0 /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.gogopackage 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.texttextmain.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.gogopackage 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.texttextfalse 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.gogovar 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.texttext4200 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.gogopackage 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.texttextfile 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.gogofunc 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.texttextmain.go:20:2: unreachable code
y staticcheck señala el propio break:
example.texttextmain.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.gogofunc 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.texttexthandled 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.gogofor _, 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.gogourls := []string{"/", "/about"} for _, u := range urls { urls = append(urls, u+"/sitemap.xml") } fmt.Println(len(urls), urls)
example.texttext4 [/ /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.gogofor 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.gogocursor := "" for { page := fetchPage(cursor) fmt.Println("fetched", page.Items) cursor = page.Next if cursor == "" { break } }
example.texttextfetched [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
- The Go Programming Language Specification, For statements: https://go.dev/ref/spec#For_statements
- The Go Programming Language Specification, Break statements: https://go.dev/ref/spec#Break_statements
- Effective Go, For: https://go.dev/doc/effective_go#for
- The Go Blog, Fixing For Loops in Go 1.22: https://go.dev/blog/loopvar-preview
- Go 1.22 Release Notes: https://go.dev/doc/go1.22
- Go 1.23 Release Notes: https://go.dev/doc/go1.23
- The Go Blog, Range Over Function Types: https://go.dev/blog/range-functions
- staticcheck SA4011: https://staticcheck.dev/docs/checks/#SA4011
