Blog

Control de flujo en Go: if, for y switch

Go se arregla con if, una sola palabra clave de bucle llamada for y un switch que no cae al siguiente caso. Aprende cada forma, el cambio de Go 1.22 que da a cada iteración su propia variable, y el break con etiqueta.

Go tiene menos palabras clave de control de flujo que la mayoría de los lenguajes. Está if, está switch, y hay exactamente una palabra clave de bucle, for. No hay while, ni do, ni foreach. Cada uno de esos trabajos es una forma distinta de for.

Este post recorre cada forma, más break, continue, las etiquetas y el cambio de Go 1.22 que arregló un bug clásico de los bucles. Cada programa de abajo se ejecutó con Go 1.26, y su salida está copiada de esa ejecución.

if sin paréntesis y con llaves obligatorias

Un if de Go no lleva paréntesis alrededor de la condición, y las llaves son siempre obligatorias, incluso para una sola línea.

package main

import "fmt"

func main() {
	temp := 31
	if temp > 30 {
		fmt.Println("hot")
	} else if temp > 15 {
		fmt.Println("mild")
	} else {
		fmt.Println("cold")
	}
}

Imprime:

hot

La condición tiene que ser un bool. Go no trata 0, una cadena vacía o nil como falso, así que if count { ... } no compila cuando count es un int. Escribes if count > 0 y dices lo que quieres decir.

if con una sentencia corta

Un if en Go puede ejecutar una sentencia corta antes de su condición, separada por un punto y coma. Vas a ver esta forma todo el tiempo, porque es como el código Go busca algo y revisa el resultado en una línea:

package main

import "fmt"

func main() {
	ages := map[string]int{"ana": 31, "ben": 0}

	if age, ok := ages["ben"]; ok {
		fmt.Println("ben is", age)
	}
	if age, ok := ages["cai"]; ok {
		fmt.Println("cai is", age)
	} else {
		fmt.Println("no age for cai, got", age)
	}
}

Imprime:

ben is 0
no age for cai, got 0

age, ok := ages["ben"] se ejecuta primero. Después ok es la condición. Leer una clave que no existe en un map da el valor cero, así que age es 0 tanto para “ben” como para “cai”. Solo ok te dice que “ben” de verdad está ahí con una edad de 0 y que “cai” no está para nada. La parte sobre maps cubre esta forma comma-ok en detalle.

Fíjate en que age también se puede usar en la rama else. Una variable declarada en la sentencia corta vive durante todo el if, incluidos cada else if y el else.

Cuando el if termina, la variable desaparece:

package main

import "fmt"

func main() {
	ages := map[string]int{"ana": 31}
	if age, ok := ages["ana"]; ok {
		fmt.Println("found", age)
	}
	fmt.Println(age)
}

No compila:

./main.go:10:14: undefined: age

Esa es la razón para usar la forma corta. La variable existe justo donde es útil y no se escapa al resto de la función, donde alguien podría leer por error un valor viejo.

for: la única palabra clave de bucle

Go tiene una sola palabra clave de bucle, for, y tiene tres formas que cubren lo que otros lenguajes reparten entre for, while y do.

package main

import "fmt"

func main() {
	// Three parts: init; condition; post.
	var seen []int
	for i := 0; i < 3; i++ {
		seen = append(seen, i)
	}
	fmt.Println(seen)

	// Condition only: this is Go's while loop.
	n := 1
	for n < 100 {
		n *= 3
	}
	fmt.Println(n)

	// No condition: loop until something breaks out.
	tries := 0
	for {
		tries++
		if tries == 4 {
			break
		}
	}
	fmt.Println("tries:", tries)
}

Imprime:

[0 1 2]
243
tries: 4

La forma de tres partes es la de C y Java, sin los paréntesis. Quita la inicialización y el paso final y tienes un bucle while. Quita también la condición y el bucle corre para siempre, hasta que un break o un return lo detenga. El bucle que acepta conexiones en un servidor y un worker que espera trabajos tienen los dos la forma de ese for pelado.

i++ es una sentencia en Go, no una expresión. No puedes escribir x := i++, y no existe ++i.

for range sobre slices y cadenas

Un bucle for range recorre los elementos de una colección y te da un índice y un valor en cada pasada.

package main

import "fmt"

func main() {
	fruits := []string{"fig", "kiwi", "plum"}
	for i, f := range fruits {
		fmt.Println(i, f)
	}

	prices := []int{10, 20, 30}
	for _, p := range prices {
		p *= 2
	}
	fmt.Println(prices)

	for i := range prices {
		prices[i] *= 2
	}
	fmt.Println(prices)
}

Imprime:

0 fig
1 kiwi
2 plum
[10 20 30]
[20 40 60]

Usa _ para la parte que no necesitas. Go se niega a compilar una variable sin usar, así que _ es como dices “ignoro el índice a propósito”.

El segundo bucle es una trampa que vale la pena ver una vez. p es una copia de cada elemento, así que duplicar p no cambia nada en el slice. Para cambiar los elementos, recorre los índices y escribe a través de prices[i], como hace el tercer bucle.

Recorrer una cadena con range te da algo un poco distinto:

package main

import "fmt"

func main() {
	for i, r := range "héllo" {
		fmt.Println(i, string(r))
	}
}

Imprime:

0 h
1 é
3 l
4 l
5 o

El índice salta de 1 a 3. Un range sobre una cadena recorre runes (puntos de código Unicode), y el índice es la posición en bytes donde empieza cada una. é ocupa dos bytes en UTF-8, así que la siguiente rune empieza en el byte 3. La parte sobre cadenas, bytes y runes explica por qué.

for range sobre maps: el orden es aleatorio a propósito

Recorrer un map con range te da cada clave y su valor, pero Go no promete ningún orden, y el runtime lo cambia a propósito de un bucle al siguiente:

package main

import "fmt"

func main() {
	stock := map[string]int{"apples": 5, "bread": 2, "cheese": 7, "dates": 1}

	for k, v := range stock {
		fmt.Println(k, v)
	}
}

Una ejecución imprimió:

cheese 7
apples 5
dates 1
bread 2

Ejecútalo otra vez y las líneas pueden salir en otro orden. Nada cambió en el map. Cada range sobre un map empieza desde una posición elegida al azar.

El equipo de Go agregó esa aleatoriedad para que los programas no pudieran depender en silencio de un orden que nunca estuvo garantizado. El código que funciona porque las claves “siempre” salen en un orden se rompe el día que ese orden cambia. Mezclarlas cada vez hace que el bug aparezca en tus pruebas.

Cuando necesitas un orden estable, pídelo. maps.Keys devuelve las claves, slices.Sorted las junta en un slice ordenado, y recorres ese slice:

package main

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

func main() {
	stock := map[string]int{"apples": 5, "bread": 2, "cheese": 7, "dates": 1}

	for _, k := range slices.Sorted(maps.Keys(stock)) {
		fmt.Println(k, stock[k])
	}
}

Imprime:

apples 5
bread 2
cheese 7
dates 1

Esta versión imprime lo mismo cada vez. La parte sobre maps profundiza en esto.

for range sobre canales

Un for range sobre un canal recibe valores hasta que el canal se cierra.

package main

import "fmt"

func main() {
	jobs := make(chan string, 3)
	jobs <- "resize"
	jobs <- "upload"
	jobs <- "notify"
	close(jobs)

	for job := range jobs {
		fmt.Println("doing", job)
	}
	fmt.Println("channel closed, loop done")
}

Imprime:

doing resize
doing upload
doing notify
channel closed, loop done

Aquí hay una sola variable de bucle, el valor. Un canal no tiene índice. Si nadie cerrara nunca jobs, el bucle esperaría para siempre un cuarto valor. Los canales tienen su propia parte más adelante en la serie. Por ahora, basta con saber que for range es como los lees.

for range sobre un entero

Desde Go 1.22, for i := range n cuenta desde 0 hasta n, sin incluirlo.

package main

import "fmt"

func main() {
	var seen []int
	for i := range 5 {
		seen = append(seen, i)
	}
	fmt.Println(seen)

	total := 0
	for i := range 4 {
		total += i
	}
	fmt.Println(total)
}

Imprime:

[0 1 2 3 4]
6

Es lo mismo que for i := 0; i < 5; i++, con menos cosas que pueden salir mal. No hay condición que escribir mal como <= ni incremento que olvidar. Ahora es la forma normal de repetir un número fijo de veces, y la vas a ver en toda esta serie.

Cada iteración tiene su propia variable de bucle

Desde Go 1.22, cada pasada por un bucle for recibe una copia nueva de la variable de bucle, y eso importa en cuanto un closure la captura.

package main

import "fmt"

func main() {
	var prints []func()
	for i := 0; i < 3; i++ {
		prints = append(prints, func() { fmt.Println(i) })
	}
	for _, p := range prints {
		p()
	}
}

Imprime:

0
1
2

El primer bucle crea tres funciones pequeñas, y cada una imprime i. Ninguna se ejecuta hasta el segundo bucle. Para entonces el primer bucle ya terminó, y aun así cada función imprime el valor que tenía i cuando se creó esa función.

Parece obvio. Antes de Go 1.22 no era así, y el mismo programa imprimía 3 3 3. Era uno de los bugs más comunes de Go, y pegaba más fuerte cuando el closure era una goroutine lanzada dentro de un bucle.

Explicado como si tuvieras diez años

Imagina que repartes tres sobres cerrados, y afuera de cada uno escribes “ábreme después para ver el número del pizarrón”.

El Go viejo tenía un solo pizarrón. Escribías 0, entregabas un sobre, lo borrabas y escribías 1, después 2, después 3. Cuando tus amigos por fin abrían sus sobres y miraban el pizarrón, todos veían 3.

El Go nuevo le da a cada sobre su propio pizarrón chiquito, engrapado adentro. El sobre uno tiene un pizarrón con 0, y nada de lo que hagas después lo toca.

La versión precisa

Un closure no copia las variables que usa. Guarda una referencia a ellas. Así que lo que importa es cuántas variables hay.

Antes de Go 1.22, un bucle for declaraba su variable una sola vez, y cada iteración actualizaba esa misma variable. Todos los closures apuntaban al único i, y todos leían su valor final, 3.

Desde Go 1.22, cada iteración declara una variable nueva. En la forma de tres partes, Go copia el valor de la iteración anterior a la variable nueva antes de ejecutar el paso final. Cada closure apunta a su propio i, que nadie cambia después de que termina esa iteración. La misma regla aplica a los bucles for range.

Dónde falla la analogía: los pizarrones chiquitos no son de solo lectura. Si el código dentro de una iteración cambia i, el closure creado en esa iteración ve el cambio. Lo que deja de pasar es que las iteraciones siguientes lo sobrescriban.

Qué regla aplica lo decide go.mod

El comportamiento nuevo depende de la línea go del go.mod de tu módulo, no de la versión de Go que tienes instalada. Ejecuta ese mismo programa con Go 1.26, en un módulo cuyo go.mod diga go 1.21, y obtienes el resultado viejo:

$ go run .
3
3
3

El compilador es nuevo, pero mantiene la semántica vieja de los bucles para los módulos viejos, para que actualizar Go no pueda cambiar lo que hace el código existente. Si copias un bucle a un proyecto viejo, revisa su go.mod. La parte sobre paquetes y módulos cubre ese archivo. Los closures en sí se ven bien en la próxima parte, sobre funciones.

break, continue y etiquetas

break sale del bucle más interno, y continue salta a su siguiente iteración.

package main

import "fmt"

func main() {
	var kept []int
	for i := range 10 {
		if i%2 == 1 {
			continue
		}
		if i > 6 {
			break
		}
		kept = append(kept, i)
	}
	fmt.Println(kept)
}

Imprime:

[0 2 4 6]

Los números impares llegan a continue, así que nunca alcanzan el append. En 8 el bucle llega a break y termina, así que 8 tampoco se imprime.

Las dos palabras clave solo alcanzan el bucle en el que están directamente. Con bucles anidados, muchas veces ese es el equivocado. Supón que buscas en una cuadrícula y quieres parar en cuanto encuentres una coincidencia. Un break normal en el bucle interno solo termina la fila actual, y el bucle externo sigue.

Una etiqueta lo resuelve. Le pones nombre al bucle externo, y después haces break o continue con ese nombre:

package main

import "fmt"

func main() {
	grid := [][]int{
		{1, 4, 7},
		{2, 5, 8},
		{3, 6, 9},
	}

	checked := 0
search:
	for r, row := range grid {
		for c, v := range row {
			checked++
			if v == 5 {
				fmt.Println("found 5 at", r, c)
				break search
			}
		}
	}
	fmt.Println("cells checked:", checked)

	for r, row := range grid {
	cells:
		for _, v := range row {
			if v%2 == 0 {
				fmt.Println("row", r, "first even:", v)
				break cells
			}
		}
	}
}

Imprime:

found 5 at 1 1
cells checked: 5
row 0 first even: 4
row 1 first even: 2
row 2 first even: 6

break search termina el bucle externo, así que la búsqueda para después de 5 celdas en vez de revisar las 9. La etiqueta va en su propia línea, seguida de dos puntos, justo antes del bucle que nombra.

El segundo bucle muestra una etiqueta en el bucle interno. Ahí break cells hace exactamente lo mismo que un break normal. Es válido pero no sirve de nada, y el código real solo etiqueta el bucle que necesita alcanzar desde más adentro. continue también acepta una etiqueta, y continue search saltaría a la siguiente fila.

Go también tiene goto, que salta a una etiqueta dentro de la misma función, pero rara vez lo vas a ver fuera del código generado.

switch: sin fallthrough por defecto

Un switch de Go ejecuta el primer caso que coincide y después para. No escribes break al final de cada caso.

package main

import "fmt"

func kind(day string) string {
	switch day {
	case "sat", "sun":
		return "weekend"
	case "mon", "tue", "wed", "thu", "fri":
		return "weekday"
	default:
		return "not a day"
	}
}

func main() {
	for _, d := range []string{"sun", "wed", "moon"} {
		fmt.Println(d, kind(d))
	}
}

Imprime:

sun weekend
wed weekday
moon not a day

Un caso puede listar varios valores, separados por comas, y coincide si cualquiera de ellos coincide. default se ejecuta cuando nada más coincidió. Es opcional, y puede ir en cualquier lugar de la lista.

En C y Java, olvidar break hace que un caso siga de largo al siguiente. Go lo dio vuelta porque el break olvidado era un bug muchas más veces que una decisión.

switch sin condición

Un switch sin nada después de la palabra clave evalúa cada caso como un booleano, y es la forma más limpia de escribir una cadena larga de if-else.

package main

import "fmt"

func grade(score int) string {
	switch {
	case score >= 90:
		return "A"
	case score >= 75:
		return "B"
	case score >= 50:
		return "C"
	default:
		return "F"
	}
}

func main() {
	for _, s := range []int{95, 75, 60, 12} {
		fmt.Println(s, grade(s))
	}
}

Imprime:

95 A
75 B
60 C
12 F

Los casos se revisan de arriba hacia abajo, y gana el primero que sea verdadero. Por eso 95 saca una A y no una B, aunque 95 >= 75 también es verdadero. Ordena los casos del más específico al menos específico.

Un switch también puede llevar una sentencia corta, igual que if: switch n := len(items); { ... } limita n al switch.

fallthrough, cuando de verdad lo quieres

La palabra clave fallthrough hace que un caso siga con el cuerpo del caso siguiente. Tienes que pedirlo explícitamente.

package main

import "fmt"

func main() {
	level := 2
	switch level {
	case 3:
		fmt.Println("send a page")
		fallthrough
	case 2:
		fmt.Println("send an email")
		fallthrough
	case 1:
		fmt.Println("write to the log")
	}
}

Imprime:

send an email
write to the log

El nivel 2 coincidió con case 2, que imprimió y después cayó a case 1. Fíjate en que fallthrough no revisa el valor del caso siguiente. Ejecuta el siguiente cuerpo sin condiciones. Esa es la sorpresa para quienes esperan que siga comparando.

fallthrough tiene que ser la última sentencia de un caso, y no se puede usar en el último caso. Rara vez lo vas a necesitar. Cuando un problema parece pedir fallthrough, un caso con varios valores o un if corto casi siempre se lee mejor.

Un primer vistazo al type switch

Un type switch elige una rama según el tipo dinámico de un valor, no según el valor en sí.

package main

import "fmt"

func describe(x any) string {
	switch v := x.(type) {
	case int:
		return fmt.Sprintf("an int, doubled is %d", v*2)
	case string:
		return fmt.Sprintf("a string of %d bytes", len(v))
	case nil:
		return "nothing at all"
	default:
		return fmt.Sprintf("something else: %T", v)
	}
}

func main() {
	fmt.Println(describe(21))
	fmt.Println(describe("go"))
	fmt.Println(describe(nil))
	fmt.Println(describe(2.5))
}

Imprime:

an int, doubled is 42
a string of 2 bytes
nothing at all
something else: float64

any significa “un valor de cualquier tipo”. Dentro de cada caso, v tiene el tipo de ese caso, así que v*2 es válido en el caso int y len(v) es válido en el caso string. La parte sobre interfaces explica qué es realmente any y cuándo un type switch es la herramienta correcta.

Qué recordar

  • Un if puede empezar con una sentencia corta, if v, ok := f(); ok. La variable vive durante toda la cadena de if y else, y en ningún otro lugar.
  • for es el único bucle de Go: de tres partes, solo con condición como un while, pelado para “siempre”, y for range sobre slices, cadenas, maps, canales y enteros.
  • El orden de recorrido de un map es aleatorio a propósito. Ordena las claves con slices.Sorted(maps.Keys(m)) cuando el orden importa.
  • Desde Go 1.22, cada iteración tiene su propia variable de bucle, así que los closures ven el valor de su propia pasada. La línea go de go.mod decide qué regla aplica.
  • break y continue alcanzan el bucle más interno. Ponle una etiqueta al bucle externo para alcanzar ese.
  • switch para después del primer caso que coincide. Usa varios valores en un caso, o un switch sin condición para una cadena de if-else. fallthrough ejecuta el siguiente cuerpo sin revisarlo.

Go tiene un solo bucle y un switch que para solo, así que hay menos que recordar y menos que hacer mal.

¿Qué tan útil te resultó este post?

¡Haz clic en un corazón para calificar!

Calificación promedio 0 / 5. Total de votos: 0

Todavía no hay votos. Sé el primero en calificar este post.