Capítulo 4. Bloques, sombras y estructuras de control
Este trabajo se ha traducido utilizando IA. Agradecemos tus opiniones y comentarios: translation-feedback@oreilly.com
Ahora que he cubierto las variables, las constantes y los tipos incorporados, estás preparado para ver la lógica y la organización de la programación. Empezaré explicando los bloques y cómo controlan cuándo está disponible un identificador. Luego presentaré las estructuras de control de Go: if
, for
, y switch
. Por último, hablaré de goto
y de la única situación en la que deberías utilizarlo.
Bloquea
Go te permite declarar variables en muchos lugares. Puedes declararlas fuera de las funciones, como parámetros de funciones y como variables locales dentro de las funciones.
Nota
Hasta ahora, sólo has escrito la función main
, pero escribirás funciones con parámetros en el próximo capítulo.
Cada lugar donde se produce una declaración se denomina bloque. Las variables, constantes, tipos y funciones declaradas fuera de cualquier función se colocan en el bloque del paquete. Has utilizado declaraciones import
en tus programas para acceder a funciones de impresión y matemáticas (y hablaré de ellas en detalle en el Capítulo 10). Definen nombres para otros paquetes que son válidos para el archivo que contiene la sentencia import
. Estos nombres están en el bloque del archivo. Todas las variables definidas en el nivel superior de una función (incluidos los parámetros de una función) están en un bloque. Dentro de una función, cada conjunto de llaves ({}
) define otro bloque, y dentro de un rato verás que las estructuras de control de Go definen bloques propios.
Puedes acceder a un identificador definido en cualquier bloque externo desde cualquier bloque interno. Esto plantea la pregunta: ¿qué ocurre cuando tienes una declaración con el mismo nombre que un identificador en un bloque contenedor? Si lo haces, ensombrecerás el identificador creado en el bloque externo.
Variables de sombra
Antes de explicar qué es el shadowing, echemos un vistazo a un poco de código (véase el Ejemplo 4-1). Puedes ejecutarlo en The Go Playground o en el directorio sample_code/shadow_variables del repositorio del Capítulo 4.
Ejemplo 4-1. Variables de sombra
func
main
()
{
x
:=
10
if
x
>
5
{
fmt
.
Println
(
x
)
x
:=
5
fmt
.
Println
(
x
)
}
fmt
.
Println
(
x
)
}
Antes de ejecutar este código, intenta adivinar lo que va a imprimir:
-
No se imprime nada; el código no se compila
-
10 en la línea uno, 5 en la línea dos, 5 en la línea tres
-
10 en la línea uno, 5 en la línea dos, 10 en la línea tres
Esto es lo que ocurre:
10 5 10
Una variable en sombra es una variable que tiene el mismo nombre que una variable de un bloque contenedor. Mientras exista la variable en sombra, no podrás acceder a una variable en sombra.
En este caso, es casi seguro que no querías crear un nuevo x
dentro de la declaración if
. En su lugar, probablemente querías asignar 5 al x
declarado en el nivel superior del bloque de funciones. En el primer fmt.Println
dentro de la sentencia if
, puedes acceder al x
declarado en el nivel superior de la función. En la línea siguiente, sin embargo, ensombreces x
declarando una nueva variable con el mismo nombre dentro del bloque creado por el cuerpo de la sentencia if
. En la segunda fmt.Println
, cuando accedes a la variable llamada x
, obtienes la variable sombreada, que tiene el valor 5. El corchete de cierre del cuerpo de la sentencia if
termina el bloque en el que existe la sombra x
, y en el tercer fmt.Println
, cuando accedes a la variable llamada x
, obtienes la variable declarada en el nivel superior de la función, que tiene el valor 10. Fíjate en que este x
no desapareció ni se reasignó; simplemente no había forma de acceder a él una vez sombreado en el bloque interior.
En el capítulo anterior mencioné que en algunas situaciones evito utilizar :=
porque puede hacer que no quede claro qué variables se están utilizando. Esto se debe a que es muy fácil ensombrecer accidentalmente una variable cuando se utiliza :=
. Recuerda que puedes utilizar :=
para crear y asignar a varias variables a la vez. Además, no todas las variables del lado izquierdo tienen que ser nuevas para que :=
sea legal. Puedes utilizar :=
siempre que haya al menos una variable nueva en el lado izquierdo. Veamos otro programa (ver Ejemplo 4-2), que puedes encontrar en The Go Playground o en el directorio sample_code/shadow_multiple_assignment del repositorio del Capítulo 4.
Ejemplo 4-2. Sombreado con asignación múltiple
func
main
()
{
x
:=
10
if
x
>
5
{
x
,
y
:=
5
,
20
fmt
.
Println
(
x
,
y
)
}
fmt
.
Println
(
x
)
}
Ejecutando este código obtienes lo siguiente:
5 20 10
Aunque ya existía una definición de x
en un bloque externo, x
seguía estando en la sombra dentro de la declaración if
. Esto se debe a que :=
sólo reutiliza las variables declaradas en el bloque actual. Cuando utilices :=
, asegúrate de que no tienes ninguna variable de un ámbito externo en el lado izquierdo, a menos que pretendas ensombrecerlas.
También tienes que tener cuidado de asegurarte en de que no haces sombra a una importación de paquetes. Hablaré más sobre la importación de paquetes en el Capítulo 10, pero has estado importando el paquete fmt
para imprimir los resultados de nuestros programas. Veamos qué ocurre cuando declaras una variable llamada fmt
dentro de tu función main
, como se muestra en el Ejemplo 4-3. Puedes intentar ejecutarlo en The Go Playground o en el directorio sample_code/shadow_package_names del repositorio del Capítulo 4.
Ejemplo 4-3. Sombreado de nombres de paquetes
func
main
()
{
x
:=
10
fmt
.
Println
(
x
)
fmt
:=
"oops"
fmt
.
Println
(
fmt
)
}
Cuando intentas ejecutar este código, aparece un error:
fmt.Println undefined (type string has no field or method Println)
Observa que el problema no es que hayas nombrado tu variable fmt
; es que has intentado acceder a algo que la variable local fmt
no tenía. Una vez declarada la variable local fmt
, hace sombra al paquete llamado fmt
en el bloque de archivos, lo que hace imposible utilizar el paquete fmt
para el resto de la función main
.
Puesto que el sombreado es útil en unos pocos casos (en capítulos posteriores se señalarán), go vet
no lo señala como un error probable. En "Uso de escáneres de calidad del código", aprenderás sobre herramientas de terceros que pueden detectar el ensombrecimiento accidental en tu código.
si
La sentencia if
en Go es muy parecida a como la sentencia if
en la mayoría de los lenguajes de programación. Como es una construcción tan familiar, la he utilizado en anteriores ejemplos de código sin preocuparme de que resultara confusa. El Ejemplo 4-5 muestra un ejemplo más completo.
Ejemplo 4-5. if
y else
n
:=
rand
.
Intn
(
10
)
if
n
==
0
{
fmt
.
Println
(
"That's too low"
)
}
else
if
n
>
5
{
fmt
.
Println
(
"That's too big:"
,
n
)
}
else
{
fmt
.
Println
(
"That's a good number:"
,
n
)
}
La diferencia más visible entre las sentencias if
en Go y en otros lenguajes es que no se ponen paréntesis alrededor de la condición. Pero Go añade otra característica a las sentencias if
que te ayuda a gestionar mejor tus variables.
Como comenté en "Variables en sombra", cualquier variable declarada dentro de las llaves de una sentencia if
o else
sólo existe dentro de ese bloque. Esto no es tan raro; ocurre en la mayoría de los lenguajes. Lo que Go añade es la posibilidad de declarar variables que se aplican a la condición y a los bloques if
y else
. Echa un vistazo al Ejemplo 4-6, que reescribe nuestro ejemplo anterior para utilizar este ámbito.
Ejemplo 4-6. Asignar una variable a una sentencia if
if
n
:=
rand
.
Intn
(
10
);
n
==
0
{
fmt
.
Println
(
"That's too low"
)
}
else
if
n
>
5
{
fmt
.
Println
(
"That's too big:"
,
n
)
}
else
{
fmt
.
Println
(
"That's a good number:"
,
n
)
}
Tener este ámbito especial es muy útil. Te permite crear variables que sólo están disponibles donde se necesitan. Una vez finalizada la serie de sentencias if/else
, n
queda indefinido. Puedes comprobarlo intentando ejecutar el código del Ejemplo 4-7 en The Go Playground o en el directorio sample_code/if_bad_scope del repositorio del Capítulo 4.
Ejemplo 4-7. Fuera de alcance...
if
n
:=
rand
.
Intn
(
10
);
n
==
0
{
fmt
.
Println
(
"That's too low"
)
}
else
if
n
>
5
{
fmt
.
Println
(
"That's too big:"
,
n
)
}
else
{
fmt
.
Println
(
"That's a good number:"
,
n
)
}
fmt
.
Println
(
n
)
Al intentar ejecutar este código se produce un error de compilación:
undefined: n
Nota
Técnicamente, puedes poner cualquier sentencia simple antes de la comparación en una sentencia if
, incluida una llamada a una función que no devuelva un valor o una asignación de un nuevo valor a una variable existente. Pero no hagas esto. Utiliza esta función sólo para definir nuevas variables que estén dentro del ámbito de las sentencias if/else
; cualquier otra cosa sería confusa.
Ten en cuenta también que, al igual que cualquier otro bloque, una variable declarada como parte de una sentencia if
hará sombra a las variables con el mismo nombre que estén declaradas en bloques que la contengan.
para, Cuatro maneras
Como en otros lenguajes de la familia C, Go utiliza una sentencia for
para hacer bucles. Lo que diferencia a Go de otros lenguajes es que for
es la única palabra clave de bucle del lenguaje. Go lo consigue utilizando la palabra clave for
en cuatro formatos:
-
Un completo, estilo C
for
-
Una condición
for
-
Un infinito
for
-
for-range
La Declaración For completa
El primer estilo de bucle for
es la declaración completa for
con la que puedes estar familiarizado de C, Java o JavaScript, como se muestra en el Ejemplo 4-8.
Ejemplo 4-8. Una declaración completa for
for
i
:=
0
;
i
<
10
;
i
++
{
fmt
.
Println
(
i
)
}
No te sorprenderá saber que este programa imprime los números del 0 al 9, ambos inclusive.
Al igual que la sentencia if
, la sentencia for
no utiliza paréntesis alrededor de sus partes. Por lo demás, debería resultarte familiar. La sentencia if
tiene tres partes, separadas por punto y coma. La primera parte es una inicialización que establece una o más variables antes de que comience el bucle. Debes recordar dos detalles importantes sobre la sección de inicialización. En primer lugar, debes utilizar :=
para inicializar las variables; var
no es legal aquí. En segundo lugar, al igual que las declaraciones de variables en las sentencias if
, aquí puedes sombrear una variable.
La segunda parte es la comparación. Debe ser una expresión que se evalúe como bool
. Se comprueba inmediatamente antes de cada iteración del bucle. Si la expresión se evalúa como true
, se ejecuta el cuerpo del bucle.
La última parte de una declaración estándar for
es el incremento. Aquí sueles ver algo como i{plus}{plus}
, pero cualquier asignación es válida. Se ejecuta inmediatamente después de cada iteración del bucle, antes de que se evalúe la condición.
Go te permite omitir una o varias de las tres partes de la sentencia for
. Lo más habitual es que omitas la inicialización si se basa en un valor calculado antes del bucle:
i
:=
0
for
;
i
<
10
;
i
++
{
fmt
.
Println
(
i
)
}
o omitirás el incremento porque tienes una regla de incremento más complicada dentro del bucle:
for
i
:=
0
;
i
<
10
;
{
fmt
.
Println
(
i
)
if
i
%
2
==
0
{
i
++
}
else
{
i
+=
2
}
}
La condición para la declaración
Cuando omitas tanto la inicialización como el incremento en una sentencia for
, no incluyas los puntos y coma. (Si lo haces, go fmt
los eliminará.) Esto deja una sentencia for
que funciona como la sentencia while
que se encuentra en C, Java, JavaScript, Python, Ruby y muchos otros lenguajes. Su aspecto es el del Ejemplo 4-9.
Ejemplo 4-9. Una declaración de sólo condición for
i
:=
1
for
i
<
100
{
fmt
.
Println
(
i
)
i
=
i
*
2
}
El Infinito por Declaración
El tercer formato de declaración for
también elimina la condición. Go tiene una versión del bucle for
que hace un bucle infinito. Si aprendiste a programar en los años 80, probablemente tu primer programa fue un bucle infinito en BASIC que imprimía HELLO
en la pantalla para siempre:
10
"HELLO"
20
GOTO
10
El Ejemplo 4-10 muestra la versión Go de este programa. Puedes ejecutarlo localmente o probarlo en The Go Playground o en el directorio sample_code/infinite_for del repositorio del Capítulo 4.
Ejemplo 4-10. Nostalgia de bucle infinito
package
main
import
"fmt"
func
main
()
{
for
{
fmt
.
Println
(
"Hello"
)
}
}
Ejecutando este programa obtienes la misma salida que llenaba las pantallas de millones de Commodore 64 y Apple ][s:
Hello Hello Hello Hello Hello Hello Hello ...
Pulsa Ctrl-C cuando estés cansado de pasear por el carril de los recuerdos.
Nota
Si ejecutas el Ejemplo 4-10 en el Patio de Juegos Go, comprobarás que detendrá su ejecución al cabo de unos segundos. Como recurso compartido, el patio de recreo no permite que ningún programa se ejecute durante demasiado tiempo.
pausa y continuar
¿Cómo se sale de un bucle infinito for
sin utilizar el teclado ni apagar el ordenador? Esa es la función de la sentencia break
. Sale del bucle inmediatamente, igual que la sentencia break
en otros lenguajes. Por supuesto, puedes utilizar break
con cualquier sentencia for
, no sólo con la sentencia infinita for
.
Nota
Go no tiene un equivalente de a la palabra clave do
en Java, C y JavaScript. Si quieres iterar al menos una vez, la forma más limpia es utilizar un bucle infinito for
que termine con una sentencia if
. Si tienes algún código Java, por ejemplo, que utilice un bucle do/while
:
do
{
// things to do in the loop
}
while
(
CONDITION
);
la versión Go tiene este aspecto:
for
{
// things to do in the loop
if
!
CONDITION
{
break
}
}
Observa que la condición tiene un !
inicial para negar la condición del código Java. El código Go especifica cómo salir del bucle, mientras que el código Java especifica cómo permanecer en él.
Go también incluye la palabra clave continue
, que se salta el resto del cuerpo del bucle for
y pasa directamente a la siguiente iteración. Técnicamente, no necesitas unacontinue
declaración. Podrías escribir un código como el del Ejemplo 4-11.
Ejemplo 4-11. Código confuso
for
i
:=
1
;
i
<=
100
;
i
++
{
if
i
%
3
==
0
{
if
i
%
5
==
0
{
fmt
.
Println
(
"FizzBuzz"
)
}
else
{
fmt
.
Println
(
"Fizz"
)
}
}
else
if
i
%
5
==
0
{
fmt
.
Println
(
"Buzz"
)
}
else
{
fmt
.
Println
(
i
)
}
}
Pero esto no es idiomático. Go fomenta los cuerpos de sentencia cortos if
, lo más alineados a la izquierda posible. El código anidado es más difícil de seguir. Utilizar una sentencia continue
facilita la comprensión de lo que ocurre. El Ejemplo 4-12 muestra el código del ejemplo anterior, reescrito para utilizar continue
en su lugar.
Ejemplo 4-12. Utilizar continue
para que el código sea más claro
for
i
:=
1
;
i
<=
100
;
i
++
{
if
i
%
3
==
0
&&
i
%
5
==
0
{
fmt
.
Println
(
"FizzBuzz"
)
continue
}
if
i
%
3
==
0
{
fmt
.
Println
(
"Fizz"
)
continue
}
if
i
%
5
==
0
{
fmt
.
Println
(
"Buzz"
)
continue
}
fmt
.
Println
(
i
)
}
Como puedes ver, sustituir las cadenas de sentencias if/else
por una serie de sentencias if
que utilizan continue
hace que las condiciones se alineen. Esto mejora la disposición de tus condiciones, lo que significa que tu código es más fácil de leer y comprender.
La declaración for-range
El cuarto formato de sentencia for
sirve para iterar sobre los elementos de algunos de los tipos incorporados de Go. Se denomina bucle for-range
y se parece a los iteradores de otros lenguajes. Esta sección muestra cómo utilizar un bucle for-range
con cadenas, matrices, rebanadas y mapas. Cuando trate los canales en el Capítulo 12, hablaré de cómo utilizarlos con los bucles for-range
.
Nota
Puedes utilizar un bucle for-range
sólo para iterar sobre los tipos compuestos incorporados y los tipos definidos por el usuario que se basan en ellos.
En primer lugar, echemos un vistazo al uso de un bucle for-range
con una rebanada. Puedes probar el código del Ejemplo 4-13 en The Go Playground o en la función forRangeKeyValue
de main.go en el directorio sample_code/for_range del repositorio del Capítulo 4.
Ejemplo 4-13. El bucle for-range
evenVals
:=
[]
int
{
2
,
4
,
6
,
8
,
10
,
12
}
for
i
,
v
:=
range
evenVals
{
fmt
.
Println
(
i
,
v
)
}
La ejecución de este código produce el siguiente resultado:
0 2 1 4 2 6 3 8 4 10 5 12
Lo que hace interesante a un bucle for-range
es que obtienes dos variables de bucle. La primera variable es la posición en la estructura de datos que se está iterando, mientras que la segunda es el valor en esa posición. Los nombres idiomáticos de las dos variables de bucle dependen de lo que se esté recorriendo. Cuando se recorre una matriz, un segmento o una cadena, se suele utilizar i
para el índice. Cuando se itera sobre un mapa, se utiliza k
(para clave).
La segunda variable se suele llamar v
por el valor, pero a veces se le da un nombre basado en el tipo de los valores que se están iterando. Por supuesto, puedes dar a las variables los nombres que quieras. Si el cuerpo del bucle sólo contiene unas pocas sentencias, los nombres de variables de una sola letra funcionan bien. Para bucles más largos (o anidados), querrás utilizar nombres más descriptivos.
¿Y si no necesitas utilizar la primera variable dentro de tu bucle for-range
? Recuerda que Go exige que accedas a todas las variables declaradas, y esta regla se aplica también a las declaradas como parte de un bucle for
. Si no necesitas acceder a la clave, utiliza un guión bajo (_
) como nombre de la variable. Esto le dice a Go que ignore el valor.
Vamos a reescribir el código de slice ranging para que no imprima la posición. Puedes ejecutar el código del Ejemplo 4-14 en The Go Playground o en la función forRangeIgnoreKey
de main.go en el directorio sample_code/for_range del repositorio del Capítulo 4.
Ejemplo 4-14. Ignorar el índice de corte en un bucle for-range
evenVals
:=
[]
int
{
2
,
4
,
6
,
8
,
10
,
12
}
for
_
,
v
:=
range
evenVals
{
fmt
.
Println
(
v
)
}
La ejecución de este código produce el siguiente resultado:
2 4 6 8 10 12
Consejo
Siempre que te encuentres en una situación en la que se devuelva un valor, pero quieras ignorarlo, utiliza un guión bajo para ocultar el valor. Volverás a ver el patrón del guión bajo cuando hable de las funciones en el Capítulo 5 y de los paquetes en el Capítulo 10.
¿Y si quieres la clave pero no quieres el valor? En esta situación, Go te permite simplemente omitir la segunda variable. Este es un código Go válido:
uniqueNames
:=
map
[
string
]
bool
{
"Fred"
:
true
,
"Raul"
:
true
,
"Wilma"
:
true
}
for
k
:=
range
uniqueNames
{
fmt
.
Println
(
k
)
}
La razón más común para iterar sobre la clave es cuando se utiliza un mapa como conjunto. En esas situaciones, el valor carece de importancia. Sin embargo, también puedes omitir el valor cuando iteras sobre matrices o rebanadas. Esto es poco frecuente, ya que la razón habitual para iterar sobre una estructura de datos lineal es acceder a los datos. Si te encuentras utilizando este formato para una matriz o porción, es muy probable que hayas elegido la estructura de datos equivocada y debas considerar la posibilidad de refactorizarla.
Nota
Cuando veas los canales en el Capítulo 12, verás una situación en la que un bucle for-range
devuelve un único valor cada vez que el bucle itera.
Iterar sobre mapas
Hay algo interesante en sobre cómo un bucle for-range
itera sobre un mapa. Puedes ejecutar el código del Ejemplo 4-15 en The Go Playground o en el directorio sample_code/iterate_map del repositorio del Capítulo 4.
Ejemplo 4-15. El orden de iteración del mapa varía
m
:=
map
[
string
]
int
{
"a"
:
1
,
"c"
:
3
,
"b"
:
2
,
}
for
i
:=
0
;
i
<
3
;
i
++
{
fmt
.
Println
(
"Loop"
,
i
)
for
k
,
v
:=
range
m
{
fmt
.
Println
(
k
,
v
)
}
}
Cuando construyes y ejecutas este programa, la salida varía. Aquí tienes una posibilidad:
Loop 0 c 3 b 2 a 1 Loop 1 a 1 c 3 b 2 Loop 2 b 2 a 1 c 3
El orden de las claves y valores varía; algunas ejecuciones pueden ser idénticas. Esto es, en realidad, una característica de seguridad. En versiones anteriores de Go, el orden de iteración de las claves de un mapa solía ser (aunque no siempre) el mismo si insertabas los mismos elementos en un mapa. Esto causaba dos problemas:
-
La gente escribía código que asumía que el orden era fijo, y este código se rompía en momentos extraños.
-
Si los mapas siempre hacen hash de los elementos con los mismos valores exactos, y sabes que un servidor almacena datos de usuario en un mapa, puedes ralentizar un servidor con un ataque llamado Hash DoS enviándole datos especialmente diseñados con claves que hacen hash al mismo cubo.
Para evitar estos dos problemas, el equipo de Go introdujo dos cambios en la implementación de los mapas. En primer lugar, modificaron el algoritmo hash de los mapas para incluir un número aleatorio que se genera cada vez que se crea una variable de mapa. Después, hicieron que el orden de una iteración for-range
sobre un mapa variara un poco cada vez que se repite el mapa. Estos dos cambios hacen que sea mucho más difícil implementar un ataque Hash DoS.
Nota
Esta regla tiene una excepción. Para facilitar la depuración y el registro de los mapas, las funciones de formato (como fmt.Println
) siempre muestran los mapas con sus claves en orden ascendente.
Iterar sobre cadenas
Como he mencionado antes, también puedes utilizar una cadena con un bucle for-range
. Echémosle un vistazo. Puedes ejecutar el código del Ejemplo 4-16 en tu ordenador o en The Go Playground o en el directorio sample_code/iterate_string del repositorio del Capítulo 4.
Ejemplo 4-16. Iterar sobre cadenas
samples
:=
[]
string
{
"hello"
,
"apple_π!"
}
for
_
,
sample
:=
range
samples
{
for
i
,
r
:=
range
sample
{
fmt
.
Println
(
i
,
r
,
string
(
r
))
}
fmt
.
Println
()
}
La salida cuando el código itera sobre la palabra "hola" no tiene sorpresas:
0 104 h 1 101 e 2 108 l 3 108 l 4 111 o
En la primera columna está el índice; en la segunda, el valor numérico de la letra; y en la tercera está el valor numérico del tipo de letra convertido a cadena.
Mirar el resultado de "manzana_π!" es más interesante:
0 97 a 1 112 p 2 112 p 3 108 l 4 101 e 5 95 _ 6 960 π 8 33 !
Observa dos cosas en este resultado. Primero, fíjate en que la primera columna se salta el número 7. En segundo lugar, el valor en la posición 6 es 960. Eso es mucho mayor de lo que cabe en un byte. Pero en el Capítulo 3 viste que las cadenas estaban formadas por bytes. ¿Qué ocurre?
Lo que estás viendo es un comportamiento especial de iterando sobre una cadena con un bucle for-range
. Recorre las runas, no los bytes. Cada vez que un bucle for-range
encuentra una runa multibyte en una cadena, convierte la representación UTF-8 en un único número de 32 bits y lo asigna al valor. El desplazamiento se incrementa en el número de bytes de la runa. Si el bucle for-range
encuentra un byte que no representa un valor UTF-8 válido, devuelve en su lugar el carácter de sustitución Unicode (valor hexadecimal 0xfffd).
Consejo
Utiliza un bucle for-range
para acceder a las runas de una cadena en orden. La primera variable contiene el número de bytes desde el principio de la cadena, pero el tipo de la segunda variable es runa.
El valor del rango for es una copia
Debes tener en cuenta que cada vez que el bucle for-range
itera sobre tu tipo compuesto, copia el valor del tipo compuesto a la variable valor. Modificar la variable valor no modificará el valor del tipo compuesto. El Ejemplo 4-17 muestra un programa rápido para demostrar esto. Puedes probarlo en The Go Playground o en la función forRangeIsACopy
de main.go en el directorio sample_code/for_range del repositorio del Capítulo 4.
Ejemplo 4-17. Modificar el valor no modifica la fuente
evenVals
:=
[]
int
{
2
,
4
,
6
,
8
,
10
,
12
}
for
_
,
v
:=
range
evenVals
{
v
*=
2
}
fmt
.
Println
(
evenVals
)
Al ejecutar este código se obtiene el siguiente resultado:
[2 4 6 8 10 12]
En las versiones de Go anteriores a la 1.22, la variable valor se crea una vez y se reutiliza en cada iteración del bucle for
. Desde Go 1.22, el comportamiento por defecto es crear una nueva variable índice y valor en cada iteración a través del bucle for
. Este cambio puede no parecer importante, pero evita un error común. Cuando hable de goroutines y bucles for-range
en "Goroutines, bucles for y variables variables", verás que antes de Go 1.22, si lanzabas goroutines en un bucle for-range
, debías tener cuidado en cómo pasabas el índice y el valor a las goroutines, o obtendrías resultados sorprendentemente erróneos.
Dado que se trata de un cambio que rompe con el pasado (aunque sea un cambio que elimina un error común), puedes controlar si se activa este comportamiento especificando la versión de Go en la directiva go
del archivo go. mod de tu módulo. Hablo de esto con más detalle en "Uso de go.mod".
Al igual que con las otras tres formas de la declaración for
, puedes utilizar break
y continue
con un bucle for-range
.
Etiquetar tus declaraciones for
Por defecto, las palabras clave break
y continue
se aplican al bucle for
que las contiene directamente. ¿Qué pasa si tienes bucles for
anidados y quieres salir o saltarte un iterador de un bucle exterior? Veamos un ejemplo. Vas a modificar el programa de iteración de cadenas anterior para que deje de iterar por una cadena en cuanto llegue a una letra "l". Puedes ejecutar el código del Ejemplo 4-18 en The Go Playground o en el directorio sample_code/for_label del repositorio del Capítulo 4.
Ejemplo 4-18. Etiquetas
func
main
()
{
samples
:=
[]
string
{
"hello"
,
"apple_π!"
}
outer
:
for
_
,
sample
:=
range
samples
{
for
i
,
r
:=
range
sample
{
fmt
.
Println
(
i
,
r
,
string
(
r
))
if
r
==
'l'
{
continue
outer
}
}
fmt
.
Println
()
}
}
Observa que la etiqueta outer
está sangrada por go fmt
al mismo nivel que la función que la rodea. Las etiquetas siempre están sangradas al mismo nivel que las llaves del bloque. Esto hace que sean más fáciles de ver. Al ejecutar el programa se obtiene el siguiente resultado:
0 104 h 1 101 e 2 108 l 0 97 a 1 112 p 2 112 p 3 108 l
Los bucles anidados for
con etiquetas son poco frecuentes. Se suelen utilizar para implementar algoritmos similares al siguiente pseudocódigo:
outer
:
for
_
,
outerVal
:=
range
outerValues
{
for
_
,
innerVal
:=
range
outerVal
{
// process innerVal
if
invalidSituation
(
innerVal
)
{
continue
outer
}
}
// here we have code that runs only when all of the
// innerVal values were successfully processed
}
Elegir lo adecuado para la declaración
Ahora que he cubierto todas las formas de la declaración for
, puede que te estés preguntando cuándo utilizar cada formato. La mayoría de las veces, vas a utilizar el formato for-range
. Un bucle for-range
es la mejor forma de recorrer una cadena, ya que te devuelve correctamente runas en lugar de bytes. También has visto que un bucle for-range
funciona bien para iterar a través de rebanadas y mapas, y verás en el Capítulo 12 que los canales también funcionan naturalmente con for-range
.
Consejo
Favorece un bucle for-range
al iterar sobre todo el contenido de una instancia de uno de los tipos compuestos incorporados. Evita una gran cantidad de código repetitivo que es necesario cuando utilizas una matriz, una rebanada o un mapa con uno de los otros estilos de bucle for
.
¿Cuándo debes utilizar el bucle completo for
? El mejor lugar para utilizarlo es cuando no estás iterando desde el primer elemento hasta el último de un tipo compuesto. Aunque podrías utilizar alguna combinación de if
, continue
, y break
dentro de un bucle for-range
, un bucle estándar for
es una forma más clara de indicar el inicio y el final de tu iteración. Compara estos dos fragmentos de código, que iteran sobre los elementos penúltimo y penúltimo de una matriz. Primero el bucle for-range
:
evenVals
:=
[]
int
{
2
,
4
,
6
,
8
,
10
}
for
i
,
v
:=
range
evenVals
{
if
i
==
0
{
continue
}
if
i
==
len
(
evenVals
)
-
1
{
break
}
fmt
.
Println
(
i
,
v
)
}
Y aquí tienes el mismo código, con un bucle estándar for
:
evenVals
:=
[]
int
{
2
,
4
,
6
,
8
,
10
}
for
i
:=
1
;
i
<
len
(
evenVals
)
-
1
;
i
++
{
fmt
.
Println
(
i
,
evenVals
[
i
])
}
El código estándar del bucle for
es más corto y más fácil de entender.
Advertencia
Este patrón no funciona para saltar el principio de una cadena. Recuerda que un bucle estándar for
no maneja correctamente los caracteres multibyte. Si quieres saltarte algunas de las runas de una cadena, tienes que utilizar un bucle for-range
para que procese correctamente las runas por ti.
Los otros dos formatos de sentencia for
se utilizan con menos frecuencia. El bucle for
de sólo condición es, al igual que el bucle while
al que sustituye, útil cuando haces un bucle basado en un valor calculado.
El bucle infinito for
es útil en algunas situaciones. El cuerpo del bucle for
debe contener siempre un break
o return
porque rara vez querrás hacer un bucle eterno. Los programas del mundo real deben limitar la iteración y fallar con elegancia cuando las operaciones no puedan completarse. Como se ha mostrado anteriormente, un bucle infinito for
puede combinarse con una sentencia if
para simular la sentencia do
presente en otros lenguajes. Un bucle infinito for
también se utiliza para implementar algunas versiones del patrón iterador, que verás cuando revise la biblioteca estándar en "io y sus amigos".
Interruptor
Como muchos lenguajes derivados de C, Go tiene una sentencia switch
. La mayoría de los desarrolladores de esos lenguajes evitan las sentencias switch
debido a sus limitaciones en cuanto a los valores que se pueden activar y al comportamiento de caída por defecto. Pero Go es diferente. Hace que las sentencias switch
sean útiles.
Nota
Para los lectores que estén más familiarizados con Go, en este capítulo voy a tratar las sentencias de conmutación de expresiones. Hablaré de las sentencias de conmutación de tipo cuando hable de las interfaces en el capítulo 7.
A primera vista, las sentencias switch
en Go no parecen muy diferentes de cómo aparecen en C/C++, Java o JavaScript, pero hay algunas sorpresas.Veamos un ejemplo de sentencia switch
. Puedes ejecutar el código del Ejemplo 4-19 en The Go Playground o en la función basicSwitch
en main.go
en el directorio sample_code/switch del repositorio del Capítulo 4.
Ejemplo 4-19. La declaración switch
words
:=
[]
string
{
"a"
,
"cow"
,
"smile"
,
"gopher"
,
"octopus"
,
"anthropologist"
}
for
_
,
word
:=
range
words
{
switch
size
:=
len
(
word
);
size
{
case
1
,
2
,
3
,
4
:
fmt
.
Println
(
word
,
"is a short word!"
)
case
5
:
wordLen
:=
len
(
word
)
fmt
.
Println
(
word
,
"is exactly the right length:"
,
wordLen
)
case
6
,
7
,
8
,
9
:
default
:
fmt
.
Println
(
word
,
"is a long word!"
)
}
}
Cuando ejecutes este código, obtendrás la siguiente salida:
a is a short word! cow is a short word! smile is exactly the right length: 5 anthropologist is a long word!
Repasaré las características de la sentencia switch
para explicar el resultado. Como en el caso de las sentencias if
, en una sentencia switch
no se ponen paréntesis alrededor del valor que se compara. Al igual que en una sentencia if
, puedes declarar una variable que se aplique a todas las ramas de la sentencia switch
. En este caso, estás aplicando la variable size
a todos los casos de la sentencia switch
.
Todas las cláusulas case
(y la cláusula opcional default
) están contenidas dentro de un conjunto de llaves. Pero debes tener en cuenta que no se ponen llaves alrededor del contenido de las cláusulas case
. Puedes tener varias líneas dentro de una cláusula case
(o default
), y se considera que todas forman parte del mismo bloque.
Dentro de case 5:
, declaras wordLen
, una nueva variable. Como se trata de un bloque nuevo, puedes declarar nuevas variables dentro de él. Al igual que cualquier otro bloque, las variables declaradas dentro de un bloque de la cláusula case
sólo son visibles dentro de ese bloque.
Si estás acostumbrado a poner una sentencia break
al final de cada case
en tus sentencias switch
, te alegrará darte cuenta de que han desaparecido. Por defecto, los casos de las sentencias switch
en Go no caen. Esto está más en línea con el comportamiento en Ruby o (si eres un programador de la vieja escuela) Pascal.
Esto nos lleva a preguntarnos: si los casos no coinciden, ¿qué haces si hay varios valores que deberían activar exactamente la misma lógica? En Go, separas las coincidencias múltiples con comas, como haces cuando coinciden 1, 2, 3 y 4 o 6, 7, 8 y 9. Por eso obtienes la misma salida tanto para a
como para cow
.
Lo que nos lleva a la siguiente pregunta: si no tienes fall-through, y tienes un caso vacío (como ocurre en el programa de ejemplo cuando la longitud de tu argumento es de 6, 7, 8 ó 9 caracteres), ¿qué ocurre? En Go, un caso vacío significa que no pasa nada. Por eso no ves ninguna salida de tu programa cuando utilizas octopus o gopher como parámetro.
Consejo
En aras de la exhaustividad, Go incluye una palabra clave fallthrough
, que permite que un caso continúe con el siguiente. Por favor, piénsatelo dos veces antes de implementar un algoritmo que la utilice. Si te ves en la necesidad de utilizar fallthrough
, intenta reestructurar tu lógica para eliminar las dependencias entre casos.
En el programa de ejemplo, conmutas el valor de un entero, pero eso no es todo lo que puedes hacer. Puedes conmutar sobre cualquier tipo que se pueda comparar con ==
, lo que incluye todos los tipos incorporados excepto los slices, mapas, canales, funciones y structs que contengan campos de estos tipos.
Aunque no necesitas poner una sentencia break
al final de cada cláusula case
, puedes utilizarlas cuando quieras salir antes de tiempo de una case
. Sin embargo, la necesidad de una sentencia break
podría indicar que estás haciendo algo demasiado complicado. Considera la posibilidad de refactorizar tu código para eliminarla.
Hay otro lugar en el que podrías encontrarte utilizando una sentencia break
dentro de una case
dentro de una switch
. Si tienes una sentencia switch
dentro de un bucle for
, y quieres salir del bucle for
, pon una etiqueta en la sentencia for
y pon el nombre de la etiqueta en break
. Si no usas una etiqueta, Go asume que quieres salir del bucle case
. Veamos un ejemplo rápido en el que quieres salir del bucle for
cuando llegue a 7. Puedes ejecutar el código del Ejemplo 4-20 en The Go Playground o en la función missingLabel
de main.go en el directorio sample_code/switch del repositorio del Capítulo 4.
Ejemplo 4-20. El caso de la etiqueta que falta
func
main
()
{
for
i
:=
0
;
i
<
10
;
i
++
{
switch
i
{
case
0
,
2
,
4
,
6
:
fmt
.
Println
(
i
,
"is even"
)
case
3
:
fmt
.
Println
(
i
,
"is divisible by 3 but not 2"
)
case
7
:
fmt
.
Println
(
"exit the loop!"
)
break
default
:
fmt
.
Println
(
i
,
"is boring"
)
}
}
}
La ejecución de este código produce el siguiente resultado:
0 is even 1 is boring 2 is even 3 is divisible by 3 but not 2 4 is even 5 is boring 6 is even exit the loop! 8 is boring 9 is boring
Eso no es lo que se pretende. El objetivo era salir del bucle for
cuando obtuviera un 7, pero se interpretó que break
salía de case
. Para resolverlo, tienes que introducir una etiqueta, igual que hiciste al salir de un bucle anidado for
. Primero, etiqueta la sentencia for
:
loop
:
for
i
:=
0
;
i
<
10
;
i
++
{
Luego utiliza la etiqueta en tu descanso:
break
loop
Puedes ver estos cambios en The Go Playground o en la función labeledBreak
de main.go en el directorio sample_code/switch del repositorio del Capítulo 4. Cuando vuelvas a ejecutarlo, obtendrás la salida esperada:
0 is even 1 is boring 2 is even 3 is divisible by 3 but not 2 4 is even 5 is boring 6 is even exit the loop!
Interruptores en blanco
Puedes utilizar las declaraciones switch
de otra forma más potente. Igual que Go te permite omitir partes de la declaración de una sentencia for
, puedes escribir una sentencia switch
que no especifique el valor con el que estás comparando. Esto se denomina conmutador en blanco. Un switch
normal sólo te permite comprobar la igualdad de un valor. Un switch
en blanco te permite utilizar cualquier comparación booleana para cada case
. Puedes probar el código del Ejemplo 4-21 en The Go Playground o en la función basicBlankSwitch
de main.go en el directorio sample_code/blank_switch del repositorio del Capítulo 4.
Ejemplo 4-21. El espacio en blanco switch
words
:=
[]
string
{
"hi"
,
"salutations"
,
"hello"
}
for
_
,
word
:=
range
words
{
switch
wordLen
:=
len
(
word
);
{
case
wordLen
<
5
:
fmt
.
Println
(
word
,
"is a short word!"
)
case
wordLen
>
10
:
fmt
.
Println
(
word
,
"is a long word!"
)
default
:
fmt
.
Println
(
word
,
"is exactly the right length."
)
}
}
Cuando ejecutes este programa, obtendrás la siguiente salida:
hi is a short word! salutations is a long word! hello is exactly the right length.
Al igual que con una sentencia switch
normal, puedes incluir opcionalmente una breve declaración de variable como parte de tu switch
en blanco. Pero a diferencia de un switch
normal, puedes escribir pruebas lógicas para tus casos. Las sentencias en blanco están muy bien, pero no abuses de ellas. Si te das cuenta de que has escrito un switch
en blanco en el que todos tus casos son comparaciones de igualdad contra la misma variable:
switch
{
case
a
==
2
:
fmt
.
Println
(
"a is 2"
)
case
a
==
3
:
fmt
.
Println
(
"a is 3"
)
case
a
==
4
:
fmt
.
Println
(
"a is 4"
)
default
:
fmt
.
Println
(
"a is "
,
a
)
}
debes sustituirla por una expresión switch
:
switch
a
{
case
2
:
fmt
.
Println
(
"a is 2"
)
case
3
:
fmt
.
Println
(
"a is 3"
)
case
4
:
fmt
.
Println
(
"a is 4"
)
default
:
fmt
.
Println
(
"a is "
,
a
)
}
Elegir entre si y conmutar
Desde el punto de vista funcional, no hay mucha diferencia entre una serie de sentencias if/else
y una sentencia en blanco switch
. Ambas permiten una serie de comparaciones. Entonces, ¿cuándo debes utilizar switch
, y cuándo una serie de sentencias if
o if/else
? Una sentencia switch
, incluso una switch
en blanco, indica que existe una relación entre los valores o comparaciones en cada caso. Para demostrar la diferencia de claridad, reescribe el programa FizzBuzz del Ejemplo 4-11 utilizando una switch
en blanco ,como se muestra en el Ejemplo 4-22. También puedes encontrar este código en el directorio sample_code/simplest_fizzbuzz del repositorio del Capítulo 4.
Ejemplo 4-22. Reescribir una serie de sentencias if
con un espacio en blanco switch
for
i
:=
1
;
i
<=
100
;
i
++
{
switch
{
case
i
%
3
==
0
&&
i
%
5
==
0
:
fmt
.
Println
(
"FizzBuzz"
)
case
i
%
3
==
0
:
fmt
.
Println
(
"Fizz"
)
case
i
%
5
==
0
:
fmt
.
Println
(
"Buzz"
)
default
:
fmt
.
Println
(
i
)
}
}
La mayoría de la gente estará de acuerdo en que ésta es la versión más legible. Ya no son necesarias las declaraciones continue
, y el comportamiento por defecto se hace explícito con el caso default
.
Por supuesto, nada en Go te impide hacer todo tipo de comparaciones no relacionadas en cada case
en un espacio en blanco switch
. Sin embargo, esto no es idiomático. Si te encuentras en una situación en la que quieres hacer esto, utiliza una serie de sentencias if/else
(o quizás considera la posibilidad de refactorizar tu código).
Consejo
Prefiere las declaraciones switch
en blanco a las cadenas if/else
cuando tengas varios casos relacionados. Utilizar switch
hace que las comparaciones sean más visibles y refuerza que se trata de un conjunto de asuntos relacionados.
goto-Sí, goto
Go tiene una cuarta sentencia de control, pero lo más probable es que nunca la utilices. Desde que Edsger Dijkstra escribió " Go To Statement Considered Harmful" en 1968, la sentencia goto
ha sido la oveja negra de la familia de la codificación. Hay buenas razones para ello. Tradicionalmente, goto
era peligrosa porque podía saltar a casi cualquier parte de un programa; podías saltar dentro o fuera de un bucle, saltarte definiciones de variables o situarte en medio de un conjunto de sentencias en una sentencia if
. Esto dificultaba la comprensión de lo que hacía un programa que utilizaba goto
.
La mayoría de los lenguajes modernos no incluyen goto
. Sin embargo, Go tiene una declaración goto
. Deberías hacer lo posible por evitar utilizarla, pero tiene algunos usos, y las limitaciones que Go le impone hacen que encaje mejor con la programación estructurada.
En Go, una sentencia goto
especifica una línea de código etiquetada, y la ejecución salta a ella. Sin embargo, no puedes saltar a cualquier parte. Go prohíbe los saltos que se salten las declaraciones de variables y los saltos que vayan a un bloque interno o paralelo.
El programa del Ejemplo 4-23 muestra dos sentencias goto
ilegales. Puedes intentar ejecutarlo en The Go Playground o en el directorio sample_code/broken_goto del repositorio del Capítulo 4.
Ejemplo 4-23. Go's goto
tiene reglas
func
main
()
{
a
:=
10
goto
skip
b
:=
20
skip
:
c
:=
30
fmt
.
Println
(
a
,
b
,
c
)
if
c
>
a
{
goto
inner
}
if
a
<
b
{
inner
:
fmt
.
Println
(
"a is less than b"
)
}
}
Al intentar ejecutar este programa se producen los siguientes errores:
goto skip jumps over declaration of b at ./main.go:8:4 goto inner jumps into block starting at ./main.go:15:11
Entonces, ¿para qué deberías utilizar goto
? Principalmente, no deberías. Las sentencias etiquetadas break
y continue
te permiten saltar fuera de bucles anidados profundamente o saltarte la iteración. El programa del Ejemplo 4-24 tiene una goto
legal y demuestra uno de los pocos casos de uso válidos. También puedes encontrar este código en el directorio sample_code/good_goto del repositorio del Capítulo 4.
Ejemplo 4-24. Una razón para utilizar goto
func
main
()
{
a
:=
rand
.
Intn
(
10
)
for
a
<
100
{
if
a
%
5
==
0
{
goto
done
}
a
=
a
*
2
+
1
}
fmt
.
Println
(
"do something when the loop completes normally"
)
done
:
fmt
.
Println
(
"do complicated stuff no matter why we left the loop"
)
fmt
.
Println
(
a
)
}
Este ejemplo es artificioso, pero muestra cómo goto
puede hacer que un programa sea más claro. En este caso sencillo, hay cierta lógica que no quieres que se ejecute en mitad de la función, pero sí al final de la misma. Hay formas de hacerlo sin goto
. Podrías establecer una bandera booleana o duplicar el código complicado después del bucle for
en lugar de tener un goto
, pero ambos enfoques tienen inconvenientes. Ensuciar tu código con banderas booleanas para controlar el flujo lógico es posiblemente la misma funcionalidad que la sentencia goto
, sólo que más verbosa. Duplicar código complicado es problemático porque hace que tu código sea más difícil de mantener. Estas situaciones son raras, pero si no puedes encontrar una forma de reestructurar tu lógica, utilizar un goto
como éste realmente mejora tu código.
Si quieres ver un ejemplo del mundo real, puedes echar un vistazo al método floatBits
del archivo atof.go del paquete strconv
de la biblioteca estándar. Es demasiado largo para incluirlo entero, pero el método termina con este código:
overflow
:
// ±Inf
mant
=
0
exp
=
1
<<
flt
.
expbits
-
1
+
flt
.
bias
overflow
=
true
out
:
// Assemble bits.
bits
:=
mant
&
(
uint64
(
1
)
<<
flt
.
mantbits
-
1
)
bits
|=
uint64
((
exp
-
flt
.
bias
)
&
(
1
<<
flt
.
expbits
-
1
))
<<
flt
.
mantbits
if
d
.
neg
{
bits
|=
1
<<
flt
.
mantbits
<<
flt
.
expbits
}
return
bits
,
overflow
Antes de estas líneas, hay varias comprobaciones de condiciones. Algunas requieren que se ejecute el código que sigue a la etiqueta overflow
, mientras que otras condiciones requieren saltarse ese código e ir directamente a out
. Dependiendo de la condición, hay sentencias goto
que saltan a overflow
o out
. Probablemente podrías idear una forma de evitar las sentencias goto
, pero todas ellas hacen que el código sea más difícil de entender.
Ejercicios
Ahora es el momento de aplicar todo lo que has aprendido sobre estructuras de control y bloques en Go. Puedes encontrar las respuestas a estos ejercicios en el repositorio del Capítulo 4.
-
Escribe un bucle
for
que ponga 100 números aleatorios entre 0 y 100 en una rebanada deint
. -
Haz un bucle sobre la porción que creaste en el ejercicio 1. Para cada valor del corte, aplica las siguientes reglas:
-
Si el valor es divisible por 2, imprime "¡Dos!".
-
Si el valor es divisible por 3, imprime "¡Tres!".
-
Si el valor es divisible entre 2 y 3, imprime "¡Seis!". No imprimas nada más.
-
Si no, imprime "No importa".
-
-
Inicia un nuevo programa. En
main
, declara una variableint
llamadatotal
. Escribe un buclefor
que utilice una variable llamadai
para iterar de 0 (inclusive) a 10 (exclusive). El cuerpo del buclefor
debe ser como sigue:total
:=
total
+
i
fmt
.
Println
(
total
)
Después del bucle
for
, imprime el valor detotal
. ¿Qué se imprime? ¿Cuál es el error más probable en este código?
Conclusión
En este capítulo se han tratado muchos temas importantes para escribir Go idiomático. Has aprendido sobre bloques, sombras y estructuras de control, y cómo utilizarlos correctamente. Llegados a este punto, eres capaz de escribir programas Go sencillos que se ajusten a la función main
. Es hora de pasar a programas más grandes, utilizando funciones para organizar tu código.
Get Aprender Go, 2ª Edición now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.