Kitabı oku: «Programación en Go», sayfa 3

Yazı tipi:

2.13 ENTRADA ESTÁNDAR DE DATOS

La entrada estándar permite a un programa de Go obtener datos desde el exterior, a través del teclado en la línea de comandos.

Go proporciona dos funciones para obtener datos desde la entrada estándar:

fmt.Scan fmt.Scanf

fmt.Scan lee los datos del teclado y los guarda en las variables pasadas en la invocación. Cada variable debe ir precedida por el símbolo ampersand, & (sabrá el por qué cuando llegue al capítulo 4, sobre apuntadores). Por ejemplo:

var edad int fmt.Print("Edad? ") fmt.Scan(&edad) fmt.Println("Tienes", edad, "años")

Ejemplo de entrada y salida estándar:

Edad? 36 Tienes 36 años

En el ejemplo anterior, si los datos introducidos no fueran un número entero válido, ignoraría la entrada:

Edad? manuel Tienes 0 años

fmt.Scanf permite especificar con más detalle el formato de la entrada, tomando como primer parámetro una cadena de texto en la que se pueden introducir los diversos verbos (como los de la tabla de la Figura 2.5), que se colocarían en sus respectivas variables, ya que tanto fmt.Scan como fmt.Scanf aceptan múltiples variables:

var hora, minuto, segundo int fmt.Print("HH:MM:SS? ") fmt.Scanf("%d:%d:%d", &hora, &minuto, &segundo) fmt.Printf("%d horas, %d minutos, %d segundos", hora, minuto, segundo)

Ejemplo de entrada y salida estándar:

HH:MM:SS? 12:34:56 12 horas, 34 minutos, 56 segundos

Capítulo 3
CONTROL DE FLUJO

Los programas de ejemplo mostrados hasta ahora seguían un “flujo secuencial”: las operaciones se ejecutan una a una, según su orden descendiente de escritura.

Como en los demás lenguajes de programación, Go permite alterar el flujo secuencial mediante los bloques de control de flujo, que se agrupan en dos tipos:

Condicionales. Permiten que un bloque de instrucciones se ejecute o no, dependiendo de si se cumple una condición en el programa.

Iterativos. Permiten que un bloque de instrucciones se ejecute repetidamente mientras se dé una condición.

Por “condición” entendemos cualquier expresión booleana que retorne true o false, como las mostradas en el capítulo anterior.

3.1 BLOQUES CONDICIONALES
3.1.1 if

El bloque if permite agrupar un conjunto de instrucciones que se ejecutarán si —y solo si— su condición asociada es true. Su estructura es:


Por ejemplo, el siguiente programa genera un número aleatorio, e informa de que el número generado es par. Dicho mensaje de información solo se mostrará si el número es realmente par:


Nótese la importación del paquete math/rand y el uso del comando rand. Int() para la generación de números aleatorios.

En el programa anterior, el mensaje dentro del bloque if se ejecutará solo si el número aleatorio es divisible por dos (el resto de su división entera es 0).

Si no se quiere considerar el valor 0 como número par, la condición del bloque if podría completarse mediante el uso de los operadores lógicos vistos en el capítulo anterior:

if valor != 0 && valor%2 == 0

3.1.2 if ... else

Un bloque if puede ser inmediatamente continuado por un bloque else, que ejecutaría el bloque de instrucciones asociado si —y solo si— la condición del if es false. Basándonos en el anterior ejemplo:



Por brevedad, se ha omitido la definición del paquete y los import.

En el programa anterior, siempre se ejecutará uno de los dos bloques en exclusiva: o el código dentro del bloque if (en caso de que valor sea divisible por dos), o el código dentro del bloque else (en caso de que valor no sea divisible por dos); pero nunca se ejecutarán los dos bloques a la vez.

En los ejemplos anteriores, la variable valor existe y es accesible durante toda la vida de la función main, incluso si solo se necesita en el contexto de los bloques if y else. Es una buena práctica restringir el ciclo de vida de una variable lo máximo que se pueda.

Go permite definir una variable dentro de un if, limitando su ciclo de vida a la evaluación de la condición y las instrucciones de los bloques if y else:


Aplicándolo al ejemplo anterior:


La diferencia es que, mientras anteriormente se podía usar la variable valor en cualquier punto de la función main(), con esta nueva forma la variable valor deja de existir después del bloque else (por ejemplo, no podríamos imprimir su valor en el mensaje de despedida).


CONSEJO: Limitar al máximo la vida de una variable nos ahorrará muchos errores en nuestros programas, algunos de ellos difíciles de detectar.

3.1.3 switch - case

Cuando una variable puede tomar múltiples valores de una enumeración concreta, y se debe realizar una acción distinta para cada uno de estos valores, la forma más concisa de expresarlo es mediante un bloque switch/case:


La orden switch también puede incluir una inicialización de variable, similar a la forma if anteriormente vista:

switch <inicialización variable> ; <expresión a evaluar>

El siguiente ejemplo hace uso del paquete runtime y sus variables globales runtime.GOARCH y runtime.GOOS para extraer información de la arquitectura y del sistema operativo en el que el programa se ejecuta. Como en muchos otros ejemplos a partir de ahora, se omite la definición del paquete, los import y la definición de la función main().



El código anterior mostraría algo similar al siguiente texto (dependiendo del ordenador en el que lo ejecutara):

La arquitectura de su procesador es x86 de 64 bits Su sistema operativo es windows

El primer bloque switch del ejemplo anterior evaluará el valor de la variable arch y mostrará un mensaje distinto dependiendo de si esta variable contiene "386" o "amd64" (valores definidos en las cabeceras de sus respectivos bloques case). El bloque default: se ejecutará si el valor de la variable arch no coincide con ninguno de los valores definidos tras cada case (por ejemplo, si su procesador fuera un ARM, comúnmente usado en tablets y teléfonos inteligentes).

La variable arch, definida en la función main, existirá y será visible desde cualquier punto de la función main (a partir de la declaración de la variable). Sin embargo, observe que el segundo bloque switch define la variable os en la misma línea en la que se comprueba (separado por punto y coma, de manera análoga al bloque if de la sección anterior). En este caso, la existencia y visibilidad de la variable os se limitará al bloque switch.

Si el lector está familiarizado con otros lenguajes de programación, como Java o C, habrá notado la ausencia de la palabra break al final de cada bloque case. En aras de la brevedad, Go la omite. Cuando finaliza el código dentro de un bloque case, el flujo del programa continúa fuera del switch.

Si por algún motivo fuera necesario que, tras acabar el código de un case, el flujo continuara por el siguiente bloque case, se debe usar el comando fallthrough. Ejemplo:


En el ejemplo anterior:

• Si letra == 'a' se mostrará Primera del abecedario.

• Si letra == 'A' se mostrará tanto Mayúscula como primera del abecedario.

Se pueden especificar diferentes valores, separados por comas, detrás de un case. Go ejecutará el código para ese caso si la expresión a comprobar coincide con alguno de esos valores:


La orden switch permite un análisis de patrones más complejo (hasta cierto punto, equivalente a varios if-else encadenados). Por ejemplo:


Observe que, en este caso, la orden switch no especifica ninguna variable, ya que la comprobación de esta se hace en el case.

3.2 ÓRDENES ITERATIVAS (BUCLES for)

Un bucle es un conjunto de órdenes que se repite. El bucle más sencillo que Go permite especificar es:


El bucle anterior es un “bucle infinito”. Si un programa llega a ese punto, el programa jamás continuará más allá del bucle for, a no ser que el bucle se rompa con la instrucción break:


El ejemplo anterior repite un bucle en el que el programa pide un carácter al usuario, y se repite hasta que el usuario introduce el carácter 'S' o 's' , momento en el que el bucle se rompe mediante la instrucción break.

La instrucción continue rompe el flujo de cada iteración de un for. En este caso, el flujo del programa salta de nuevo al inicio del bucle, sin ejecutar las instrucciones restantes de la iteración en que se invoca.



El ejemplo anterior se comportaría de la siguiente manera:

Salir? (s/n): a carácter no reconocido Salir? (s/n): n Salir? (s/n): s adiós!

De acuerdo con el código del ejemplo:

• Si el usuario introduce 'N' o 'n', se ejecutará la instrucción continue, por lo que la ejecución volverá al inicio del for.

• Si el usuario introduce 'S' o 's', se ejecutará la instrucción break, por lo que el for finaliza y la ejecución continúa por el mensaje adiós!

• Solo en el caso de que el usuario introdujera cualquier otro carácter, se mostraría el mensaje carácter no reconocido.

A pesar de lo que promulgan algunas escuelas de programación de corte académico, las órdenes break y continue son totalmente válidas y aceptadas en las convenciones sobre estilo de Go. Sin embargo, a menudo resulta más limpia y útil la forma condicional de for:


Por ejemplo:


El programa anterior repetirá el código dentro del bucle mientras el usuario no introduzca 'S' ni 's'.

El lector que esté familiarizado con otros lenguajes de programación, se habrá dado cuenta de que esta forma de for “condicional” suele llamarse while en otros lenguajes de programación. Los diseñadores de Go decidieron que el código resultaría mucho más simple y legible si cualquier bucle usaba la misma orden for.

Hay una tercera forma de describir un for; la más similar al bucle for de los demás lenguajes:


Este tipo de for:

1. Ejecuta la orden de inicio una sola vez, antes de ejecutar la primera iteración.

2. Antes de cada iteración, se comprobará si la condición es cierta; en caso de que no lo sea, el bucle acabará.

3. Después de cada iteración, antes de volver a comprobar si la condición sigue siendo cierta, se ejecutará la orden de actualización.

Por ejemplo, el siguiente bucle for mostrará una cuenta del 1 al 10:

for i := 1; i <= 10; i++ { fmt.Println(i) }

4. La variable i se inicia al valor 1 (observe que el alcance de esta se limita al bucle).

5. Se muestra el valor de la variable i.

6. Se incrementa la variable i.

7. Se comprueba si el valor de i es menor o igual que 10. Si es el caso, se vuelve al paso 2. Si no es el caso, se sale del bucle for.

En el capítulo 6, sobre estructuras de datos lineales, se mostrará otro uso del bucle for, comúnmente conocido como for-each, que facilita el recorrido por los valores de diferentes colecciones de datos.

3.3 CONTEXTO Y OCULTACIÓN DE VARIABLES

En cada instrucción condicional o cada bucle, cada vez que se define un nuevo contexto entre un par de llaves { y } es posible definir nuevas variables cuya vida y visibilidad se limitarán a ese contexto:


Si, en el ejemplo anterior, intentara mostrar el contenido de la variable i desde fuera del bloque if donde ha sido definida, el compilador le mostraría un mensaje de error.

Una característica de Go, que puede ser útil pero también puede ser una fuente de problemas, es el poder definir nuevas variables con nombres que ya existen en los contextos más globales. A pesar de tener el mismo nombre, serán variables distintas. Este concepto es conocido como “ocultación” o, en inglés, shadowing.

Ejemplo:


En un despiste, se podría pensar que la salida estándar de este programa sería a = 2, b = 2. Sin embargo, es a = 0, b = 2, ya que la variable a ha sido redeclarada y ocultada (eclipsada) dentro de la condición (observe el sutil detalle del operador de declaración := usado con a, frente al de asignación = usado con b). Por tanto, la variable a mostrada en fmt.Printf no ha sido modificada en ningún momento.

Capítulo 4
APUNTADORES

La memoria de un ordenador podría abstraerse, como si fuera un conjunto de cajones colocados en una gigantesca estantería. Cada cajón tiene un número identificativo único, que permite al ordenador referirse a él a la hora de ir a buscar o guardar datos. Dicho número identificativo se conoce como “dirección de memoria”.

Los apuntadores (o punteros) son variables que no guardan valores útiles como tales, sino las direcciones de memoria donde se encuentran dichos valores. Su nombre hace referencia al hecho de que no guardan una variable sino que “apuntan” a su dirección.

En Go, los apuntadores tienen un tipo asociado, es decir, solo pueden apuntar a variables de un tipo en concreto: un puntero a int solo podrá guardar la dirección de memoria de una variable del tipo int, un puntero a bool solo podrá guardar la dirección de memoria de un bool, etc.

4.1 DEFINICIÓN DE UN APUNTADOR

Un apuntador se define como una variable, añadiendo un asterisco delante del tipo de datos al que este apuntador puede apuntar:

var pi *int // apuntador a ints var pb *bool // apuntador a bools

4.2 LA REFERENCIA A nil

Los apuntadores definidos en la sección anterior no han sido inicializados a ningún valor. Apuntan al valor nil, que podría interpretarse como “a ninguna parte”.

El valor nil se puede utilizar tanto para reiniciar el valor de un apuntador “a nada” como para comprobar si un apuntador apunta a algún lugar válido:


Cuando un apuntador hace referencia a la dirección nil, este no se puede utilizar para leer o modificar valores apuntados, ya que dicho valor apuntado no existe. Si tratáramos de hacerlo, el sistema de memoria segura de Go abortaría la ejecución del programa, mostrando un error similar al siguiente:

panic: runtime error: invalid memory address or nil pointer dere- ference [signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x49169f]

4.3 APUNTANDO HACIA UNA VARIABLE

El operador ampersand (&) delante de una variable retorna la dirección de memoria de esta. Este valor se puede asignar directamente a un puntero:

i := 10 var p *int p = &i

En el ejemplo anterior, el apuntador p apuntará a la variable i (Figura 4.1). El código anterior se puede abreviar de la siguiente manera:

i := 10 p := &i

Ya que &i retorna un valor del tipo *int, p será declarado como *int (apuntador a int) y desde el principio apuntará al int i.


Figura 4.1 Apuntador “p” apuntando a la dirección de memoria de “i” (esquema visual).

4.4 LEYENDO O MODIFICANDO EL VALOR APUNTADO

A través de un apuntador, se puede leer o modificar el valor de la variable apuntada. El operador asterisco * delante del nombre del apuntador nos permitirá acceder al valor de la variable apuntada, tanto para su lectura como para su modificación:

i := 10 p := &i a := *p *p = 21

En el ejemplo anterior, donde el apuntador p apunta a la variable i, operando a través de este apuntador se está leyendo y modificando el valor de la variable i. La tercera línea (a := *p) tendría un resultado equivalente a a := i, y la cuarta línea (*p = 21) tendría un resultado equivalente a i = 21. La Figura 4.2 esquematiza la evolución de la memoria según las anteriores instrucciones se van ejecutando: qué variables se van creando y qué valores toman en cada momento.


Figura 4.2 Uso del operador asterisco sobre punteros (esquema visual).

Viéndolo desde el punto de vista del ejemplo anterior, podría parecer que los apuntadores son un paso intermedio innecesario. Más adelante, este libro muestra la verdadera utilidad de los apuntadores cuando se usan para recorrer complejas estructuras de datos, para referirse a alguna de sus partes o para intercambiar datos con “funciones”.

4.5 VALORES VERSUS REFERENCIAS

Las variables de Go, por defecto, son operadas “por valor”. Esto significa que:

• El operador de asignación = copia el valor de la derecha hacia el valor de la variable de la izquierda. Tras la instrucción a = b, a y b serán dos variables con el mismo valor, pero no son la misma variable: ocupan zonas distintas de la memoria y la modificación de una no afecta al valor anterior de la otra.

• El operador de igualdad == entre dos variables resulta en true si ambas variables tienen un valor igual, aunque sean dos variables distintas.

Los punteros de Go nos permitirán operar “por referencia”, es decir:

• El operador de asignación = apunta (o referencia) el apuntador de la izquierda hacia el apuntador de la derecha. Tras la instrucción p = &i, el apuntador p apunta hacia la misma dirección de memoria donde está i. Cualquier modificación de p afectará a tal valor y a cómo se accede a través de i.

• El operador de igualdad == entre dos apuntadores resulta en true si ambos apuntan a la misma dirección de memoria, o en false si apuntan a direcciones distintas (aunque esas direcciones contengan el mismo valor). Si se quisiera comparar la igualdad de dos valores apuntados por los apuntadores p1 y p2, se debería usar el operador asterisco para obtener el valor de ambos y, así, poder comparar valores en vez de direcciones:



El ejemplo anterior mostraría la siguiente salida estándar:

p1 y p2 apuntan a direcciones distintas p1 y p2 apuntan a valores iguales

Ya que p1 y p2 apuntan a dos variables con el mismo valor, pero en distintas zonas de memoria (Figura 4.3), tan solo mostraría el mensaje p1 y p2 apuntan a valores iguales, ya que apuntan a dos variables iguales, pero que no son la misma.


Figura 4.3 p1 y p2 apuntan a valores iguales, pero distintas variables (esquema visual).

La primera comprobación sería “por referencia”, ya que se comparan direcciones de memoria; mientras que la segunda comprobación sería “por valor”, ya que se comparan dos valores concretos.

Capítulo 5
FUNCIONES

Las funciones permiten reutilizar partes de código cuya ejecución puede invocarse desde diversos puntos del programa.

5.1 DEFINICIÓN E INVOCACIÓN

Un ejemplo sencillo de función se definiría de este modo:

func <Nombre Función>() { // código a ejecutar en cada invocación }

Por ejemplo, el siguiente código definiría una función llamada Hola que, al invocarse, muestra por pantalla el mensaje ¡Hola!:

func Hola() { fmt.Println("¡Hola!") }

Las funciones se definen fuera del cuerpo de la función main, y suelen invocarse desde el código de las propias funciones, mediante el nombre de la función seguido de dos paréntesis ():


Si se añade la definición de la función Hola al mismo archivo que contiene la función main anterior y se ejecuta el programa, la salida estándar sería:

Invocando una función: ¡Hola! Invocando la misma función, otra vez: ¡Hola!

Aunque una función ejecuta siempre el mismo código, se puede modificar ligeramente su comportamiento si se definen argumentos (o parámetros) entre los paréntesis de la cabecera de la función. Cada argumento consiste en un nombre seguido de un tipo (como si de una variable se tratara). Los diferentes argumentos se separan por comas.

Por ejemplo, se podría modificar el comportamiento de la anterior función Hola para especificar el nombre y apellidos de la persona a saludar mediante argumentos:

func Hola(nombre string, apellido string) { fmt.Printf("¡Hola, %s %s!\n", nombre, apellido) }

Para invocar una función con argumentos, entre los paréntesis de la invocación deberán situarse los valores para dichos argumentos, separados por comas, y ordenados según la definición en la cabecera:

func main() { Hola("Marta", "García") Hola("Juan", "Martínez") }

Salida estándar:

¡Hola, Marta García! ¡Hola, Juan Martínez!

Los valores a pasar como argumentos pueden ser valores literales, variables, u otras expresiones combinadas mediante operadores.


CONSEJO: Cuando en la definición de la cabecera de una función varios argumentos consecutivos son del mismo tipo, se puede omitir el tipo de los demás argumentos, excepto el último. La cabecera aconsejable de la función Hola sería:

func Hola(nombre, apellido string)

Go asumirá que tanto nombre como apellido son del tipo string.

Ücretsiz ön izlemeyi tamamladınız.