Skip to content

Latest commit

 

History

History
516 lines (432 loc) · 19.7 KB

File metadata and controls

516 lines (432 loc) · 19.7 KB

2.3 Sentencias de control y funciones

En esta sección, vamos a hablar sobre sentencias de control de flujo y operaciones con funciones en Go.

Sentencias de control

El mayor invento en los lenguajes de programación es el control de flujo. Gracias a ellos, podemos utilizar algunas sentencias sencillas de control para representar lógicas complejas. Tenemos tres categorías, condicionales, controles de ciclos y saltos incondicionales.

if

if es la más común de las palabras reservadas en sus programas. Si se cumplen las condiciones entonces realiza algo, si no realiza otra cosa o nada.

if no necesita paréntesis en Go.

	if x > 10 {
    	fmt.Println("x es mayor que 10")
	} else {
    	fmt.Println("x es menor que 10")
	}

Lo más útil de if en Go es que puede tener una instrucción de inicialización antes de la sentencia de condición. El alcance o ámbito de las variables de inicialización que nosotros definimos en esta condición es unicamente dentro del bloque del if.

	// inicializamos x, entonces verificamos si x es mayor que 10
	if x := computedValue(); x > 10 {
    	fmt.Println("x es mayor que 10")
	} else {
    	fmt.Println("x es menor que 10")
	}

	// el siguiente código no va a compilar
	fmt.Println(x)

Utiliza if-else para múltiples condiciones.

	if entero == 3 {
    	fmt.Println("entero es igual a 3")
	} else if entero < 3 {
    	fmt.Println("entero es menor que 3")
	} else {
    	fmt.Println("entero es mayor que 3")
	}

goto

Go la palabra reservada goto, se cuidadoso cuando la usas. goto tiene un salto hacia la etiqueta en el cuerpo de el mismo bloque de código.

	func myFunc() {
    	i := 0
	Here:   // label termina con ":"
    	fmt.Println(i)
    	i++
    	goto Here   // salta hacia el label "Here"
	}

El nombre de la etiqueta diferencia entre mayúsculas y minúsculas.

for

for es la lógica de control mas poderosa en Go, puede leer los datos en los ciclos y realizar operaciones iterativas, al igual que un típico while.

	for expression1; expression2; expression3 {
    	//...
	}

expression1, expression2 y expression3 son obviamente todas expresiones, donde expression1 y expression3 son definiciones de variables o valores de retornos de funciones, y expression2 es una sentencia condicional. expression1 se ejecutara siempre antes de cada bucle, y expression3 después.

Un ejemplo es mas útil que cientos de palabras.

	package main
	import "fmt"

	func main(){
    	sum := 0;
    	for index:=0; index < 10 ; index++ {
        	sum += index
    	}
    	fmt.Println("la suma es igual a ", sum)
	}
	// Print:sum es igual a 45

A veces necesitamos asignaciones múltiples, Go tiene el operador ,, así que podemos usar la asignación paralela como i, j = i + 1, j - 1.

Si no son necesarios, podemos omitir a expression1 y expression3.

	sum := 1
	for ; sum < 1000;  {
    	sum += sum
	}

Podemos omitir también el ;. ¿Se siente familiar? Si, es un while.

	sum := 1
	for sum < 1000 {
    	sum += sum
	}

Hay dos operaciones importantes en los ciclos que son break y continue. break salta afuera del ciclo, y continue salta el ciclo actual y continua en el siguiente. Si usted anida ciclos, utilice break para saltar al bucle que esta junto.

	for index := 10; index>0; index-- {
    	if index == 5{
        	break // o continue
    	}
    	fmt.Println(index)
	}
	// break imprime 10、9、8、7、6
	// continue imprime 10、9、8、7、6、4、3、2、1

`for` podría leer los datos desde un `segmento` o un `mapa` cuando es utilizado con `range`.

	for k,v:=range map {
    	fmt.Println("llave del mapa:",k)
    	fmt.Println("valor del mapa:",v)
	}

Como Go soporta el retorno de valores múltiples y nos da errores de compilación cuando utiliza valores que no fueron definidos, por eso puede necesitar utilizar _ para descartar algunos valores de retorno.

	for _, v := range map{
    	fmt.Println("valor del mapa:", v)
	}

switch

A veces puedes pensar que estas utilizando demasiados valores if-else para implementar alguna lógica, también puedes pensar que no se ve bien y que no sea correcto para mantener a futuro. Ahora es tiempo para utilizar switch para resolver este problema.

	switch sExpr {
	case expr1:
    	algunas instrucciones
	case expr2:
    	algunas otras instrucciones
	case expr3:
    	algunas otras instrucciones
      default:
    	otro código
	}

El tipo de sExpr, expr1, expr2, y expr3 debe ser el mismo. switch es muy flexible, las condiciones no necesitan ser constantes, es ejecutado de arriba hacia abajo hasta que se cumpla alguna condición. Si no hay ninguna declaración después de la palabra reservada switch, entonces este se compara con true.

	i := 10
	switch i {
	case 1:
    	fmt.Println("i es igual a 1")
	case 2, 3, 4:
    	fmt.Println("i es igual a 2, 3 o 4")
	case 10:
    	fmt.Println("i es igual a 10")
	default:
    	fmt.Println("Todo lo que yo se, es que i es un entero")
	}

En la quinta linea, pusimos muchos valores en un solo case, y no necesitamos utilizar break en el final del cuerpo de un case. Saltara fuera del cuerpo de switch una vez que coincida con algún case y ejecute las instrucciones dentro del mismo. Si usted busca que siga comparando con otros cases, va a necesitar utilizar la palabra reservada fallthrough.

	integer := 6
	switch integer {
		case 4:
			fmt.Println("integer <= 4")
			fallthrough
		case 5:
			fmt.Println("integer <= 5")
			fallthrough
		case 6:
			fmt.Println("integer <= 6")
			fallthrough
		case 7:
			fmt.Println("integer <= 7")
			fallthrough
		case 8:
			fmt.Println("integer <= 8")
			fallthrough
		default:
			fmt.Println("default case")
	}

Este programa va a imprimir la siguiente información.

	integer <= 6
	integer <= 7
	integer <= 8
	default case

Funciones

Utilizamos la palabra reservada func para definir funciones.

	func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
    	// cuerpo de la función
    	// retorna múltiples valores
    	return value1, value2
	}

Podemos obtener la siguiente información del ejemplo anterior.

  • Utilizamos la palabra reservada func para definir la función llamada funcName.
  • Funciones tienen cero, uno o mas de un argumento, el tipo del argumento después del nombre del mismo y separados por ,.
  • Las funciones pueden devolver múltiples valores.
  • El ejemplo iene dos valores de retorno llamados output1 y output2, se pueden omitir los nombre y utilizar unicamente los tipos.
  • Si solo hay un valor de retorno y omite el nombre, no va a necesitar comas para retornar mas valores.
  • Si la función no tiene valores de retorno, puede omitir la parte de retorno.
  • Si la función tiene valores de retorno, va a necesitar utilizar return en alguna parte del cuerpo de la función.

Veamos un ejemplo práctico. (calcular el valor mínimo)

	package main
	import "fmt"

	// devolvemos el valor mas grande entre a y b
	func max(a, b int) int {
    	if a > b {
        	return a
    	}
    	return b
	}

	func main() {
    	x := 3
    	y := 4
    	z := 5

    	max_xy := max(x, y) // llama a la función max(x, y)
    	max_xz := max(x, z) // llama a la función max(x, z)

    	fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
    	fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
    	fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // llamamos a la función aquí
	}

En el ejemplo anterior, tenemos dos argumentos en la función max, los de tipo int, por eso el primer tipo puede ser omitido, como a, b int en lugar de a int, b int. Se cumple la misma regla para mas argumentos. Nótese que max tiene solo un valor de retorno, por lo que solo escribimos el tipo de valor de retorno, esta es una forma corta.

Retorno de múltiples valores

Una de las cosas en las que Go es mejor que C es que soporta el retorno de múltiples valores.

Vamos a utilizar el ejemplo anterior aquí.

	package main
	import "fmt"

	// retorna el resultado de A + B y A * B
	func SumAndProduct(A, B int) (int, int) {
    return A+B, A*B
	}

	func main() {
    	x := 3
    	y := 4

    	xPLUSy, xTIMESy := SumAndProduct(x, y)

    	fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
    	fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
	}

En el ejemplo anterior devolvemos dos valores sin nombre, y tu también puedes nombrarlos. Si nombramos los valores de retorno, solo vamos a utilizar return para devolver para devolver bien los valores ya que se inicializan en la función automáticamente. Debes tener en cuenta que si tus funciones se van a utilizar fuera del paquete, lo que significa que los nombre de las funciones inician con mayúsculas, es mejor que escriba la sentencia mas completa para return; esto hace que es código sea mas legible.

	func SumAndProduct(A, B int) (add int, Multiplied int) {
    	add = A+B
    	Multiplied = A*B
    	return
	}

Argumentos variables

Go soporta funciones con un número variable de argumentos. Estas funciones son llamadas "variadic", lo que significa que puede darle un numero incierto de argumentos a la función.

	func myfunc(arg ...int) {}

arg …int le dice a Go que esta función tiene argumentos variables. Ten en cuenta que estos argumentos son de tipo int. En el cuerpo de la función , arg será un segmento de enteros.

	for _, n := range arg {
    	fmt.Printf("Y el número es: %d\n", n)
	}

Pasando argumentos por valor y punteros

Cuando pasamos argumentos a una función a la cual llamamos, esa función actualmente toma una copia de nuestra variables, cualquier cambio que realicemos no va a afectar a la variable original.

Vamos a ver un ejemplo para probar lo que decimos.

	package main
	import "fmt"

	// función sencilla para sumar 1 a 'a'
	func add1(a int) int {
    	a = a+1 // cambiamos el valor de a
    	return a // devolvemos el nuevo valor de a
	}

	func main() {
    	x := 3

    	fmt.Println("x = ", x)  // debe imprimir "x = 3"

    	x1 := add1(x)  // llamamos a add1(x)

    	fmt.Println("x+1 = ", x1) // debe imprimir "x+1 = 4"
    	fmt.Println("x = ", x)    // debe imprimir "x = 3"
	}

¿Viste eso? Siempre que llamamos a add1, y add1 le suma uno a a, el valor de x no sufre cambios.

El motivo de esto es muy sencillo: cuando llamamos a add1, nosotros obtenemos una copia de x para esto, no x en si mismo.

Ahora nos debemos preguntar, como puedo pasar el verdadero valor de x a la función.

Para eso necesitamos utilizar punteros. Sabemos que las variables son almacenadas en memoria, y todas ellas tienen una dirección de memoria, nosotros vamos a cambiar el valor de esa variables si cambiamos los valores en la dirección de memoria de esa variable. Por lo tanto la función add1 tiene que saber la dirección de memoria de x para poder cambiar su valor. Por eso necesitamos pasar de la siguiente forma su valor &x a la función, y cambiar el tipo de argumento a uno de tipo puntero *int. Tiene que ser consciente de que pasamos una copia del puntero, no una copia del valor.

	package main
	import "fmt"

	// función sencilla para sumar 1 a a
	func add1(a *int) int {
    	*a = *a+1 // cambiamos el valor de  a
    	return *a // devolvemos el nuevo valor de a
	}

	func main() {
    	x := 3

    	fmt.Println("x = ", x)  // debe imprimir "x = 3"

    	x1 := add1(&x)  // llamamos a add1(&x) pasando la dirección de memoria de x

    	fmt.Println("x+1 = ", x1) // debe imprimir "x+1 = 4"
    	fmt.Println("x = ", x)    // debe imprimir "x = 4"
	}

Ahora podemos cambiar el valor de x en la función. ¿Por qué usamos punteros? ¿Cuál es la ventaja?

  • Usamos mas funciones que modifiquen una misma variable.
  • Tiene un bajo costo utilizar direcciones de memoria (8 bytes), la copia no es una forma eficiente tanto en el tiempo como en el espacio para pasar variables.
  • Las cadenas, segmentos y mapas son tipos de referenciados, por eso ellos por defecto utilizan punteros cuando se pasan a funciones. (Atención: si necesitas cambiar el tamaño de un segmento, vas a necesitar pasar el puntero de forma explicita)

defer

Go tiene una palabra reservada muy bien diseñada llamada defer, puedes tener muchas declaraciones de defer en una función; ellas se ejecutan en orden inverso cuando el programa ejecuta el final de la función. Es útil especialmente cuando el programa abre un recurso como un archivo, estos archivos tienen que ser cerrados antes de que la función devuelva un error. Vamos a ver algún ejemplo.

	func ReadWrite() bool {
    	file.Open("file")
	// realizamos alguna tarea
    	if failureX {
        	file.Close()
        	return false
    	}

    	if failureY {
        	file.Close()
        	return false
        }

    	file.Close()
    	return true
	}

Vimos la repetición de código en varias ocasiones, defer nos va a resolver muy bien este problema. No solo nos va a ayudar a realizar código limpio, y también lo va a hacer mas legible.

	func ReadWrite() bool {
    	file.Open("file")
    	defer file.Close()
    	if failureX {
        	return false
    	}
    	if failureY {
        	return false
    	}
    	return true
	}

Si hay mas de un defer, ellos se van a ejecutar en orden inverso. El siguiente ejemplo va a imprimir 4 3 2 1 0.

	for i := 0; i < 5; i++ {
    	defer fmt.Printf("%d ", i)
	}

Funciones como valores y tipos

Las funciones son también variables en Go, podemos usar un type para definirlas. Las funciones que tienen la misma firma pueden verso como del mismo tipo.

	type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

¿Cuál es la ventaja de esta característica? La respuesta es que podemos pasar funciones como valores.

	package main
	import "fmt"

	type testInt func(int) bool // definimos un función como un tipo de variable

	func isOdd(integer int) bool {
    	if integer%2 == 0 {
        	return false
    	}
    	return true
	}

	func isEven(integer int) bool {
    	if integer%2 == 0 {
        	return true
    	}
    	return false
	}

	// pasamos la función `f` como un argumento de otra función

	func filter(slice []int, f testInt) []int {
    	var result []int
    	for _, value := range slice {
        	if f(value) {
            	result = append(result, value)
        	}
    	}
    	return result
	}

	func main(){
    	slice := []int {1, 2, 3, 4, 5, 7}
    	fmt.Println("slice = ", slice)
    	odd := filter(slice, isOdd)    // usamos la función como un valor
    	fmt.Println("Odd elements of slice are: ", odd)
    	even := filter(slice, isEven)
    	fmt.Println("Even elements of slice are: ", even)
	}

Es muy útil cuando usamos interfaces. Como puedes ver testInt es una variable que tiene como tipo una función, y devuelve valores y el argumento de filter es el mismo que el de testInt. Por lo tanto, tenemos una lógica mas compleja en nuestro programa, y hacemos nuestro código mas flexible.

Panic y Recover

Go no tiene estructura try-catch como lo tiene Java. En vez de lanzar excepciones, Go usa panic y recover para hacer frente a los errores. Sin embargo, no debería usar mucho panic, aunque sea muy poderoso.

Panic es una función incorporada para romper el flujo normal del programa y entrar en un estado de pánico. Cuando la función F llama a panic, la función F no continuara ejecutándose, pero sus funciones defer siempre se ejecutaran. Entonces F vuelve a su punto de ruptura donde se causo el estado de pánico. El programa no va a finalizar hasta que todas las funciones retornen con panic hasta el primer nivel de esa goroutine. panic se puede producir con una llamada a panic en el programa, y algunos errores pueden causar una llamada a panic como un intento de acceso fuera de un array.

Recover es una función incorporada para recuperar una goroutine de un estado de pánico, solo el llamado a recover es útil en una función defer porque las funciones normales no van a ser ejecutadas cuando el programa se encuentre en un estado de pánico. Vamos a poder atrapar el valor de panic si el programa se encuentra en un estado de pánico, este nos va a devolver nil si el programa se encuentra en un estado normal.

El siguiente ejemplo nos muestra como utilizar panic.

	var user = os.Getenv("USER")

	func init() {
    	if user == "" {
        	panic("no hay valor para $USER")
    	}
	}

El siguiente ejemplo nos muestra como verificar un panic.

	func throwsPanic(f func()) (b bool) {
    	defer func() {
        	if x := recover(); x != nil {
            	b = true
        	}
    	}()
    	f() // si f causa un panic, este va a recuperase (recover)
    	return
	}

La función main y la función init

Go tiene dos retenciones (retention) que son llamadas main e init, donde init puede ser usada en todos los paquetes y main solo puede ser usada en el paquete main. Estas dos funciones no son capaces de tener argumentos o valores de retorno. A pesar de que podemos escribir muchas funciones init en un mismo paquete, yo recomiendo fuertemente que escriban solo una función init por cada paquete.

Los programas en Go van a llamar a init() y a main() automáticamente, así que no es necesario llamarlas. Para cada paquete, la función init es opcional, pero package main tiene una y solo una función main.

Los programas se inicializan y se ejecutan desde el paquete main, si el paquete main importa otros paquetes, ellos serán importados en tiempo de compilación. Si un paquete es importado muchas veces, este va a ser compilado solo una vez. Después de importar los paquetes, el programa va a inicializar las constantes y variables en los paquetes importados, luego va a ejecutar la función init si es que existe, y así sucesivamente. Después de que todos los paquetes fueran inicializados, el programa va a comenzar a inicializar las constantes y variables en el paquete main, entonces va a ejecutar la función init en el paquete si es que existe. La siguiente figura les va a mostrar el proceso.

Figure 2.6 Flujo de inicialización de un programa en Go

import

Nosotros usamos import muy frecuentemente en los programas en Go como se muestra acá.

	import(
    	"fmt"
	)

Entonces nosotros podemos usar las funciones de ese paquete de la siguiente forma.

fmt.Println("Hola mundo")

fmt es parte de la librería estándar de Go , esta localizado en $GOROOT/pkg. Go utiliza las siguiente dos formas para paquete de terceros.

  1. Path relativo import "./model" // carga el paquete que se encuentra en el mismo directorio, yo no recomiendo utilizar esta forma.
  2. Path absoluto import "shorturl/model" // carga un paquete en el path "$GOPATH/pkg/shorturl/model"

Tenemos algunos operadores especiales para importar paquetes, y los principiantes normalmente se confunden con estos operadores.

  1. EL operador punto . A veces podemos ver personas que utilizan la siguiente forma para importar paquetes.

     import(
     	. "fmt"
     )

    El operador punto significa que podemos omitir el nombre del paquete cuando llamamos a las funciones del mismo. En vez de esto fmt.Printf("Hola mundo") ahora podemos usar esto Printf("Hola mundo").

  2. El operador alias. Este puede cambiar el nombre del paquete que vamos a importar cuando llamamos a las funciones del mismo.

     import(
     	f "fmt"
     )

    En vez de esto fmt.Printf("Hola mundo") ahora podemos usar esto f.Printf("Hola mundo").

  3. El operador _. Este es un operador un poco difícil de comprender si no tenemos una breve explicación.

     import (
     	"database/sql"
     	_ "github.com/ziutek/mymysql/godrv"
     )

    El operador _ en realidad significa que solo importamos ese paquete, y usamos la función init de ese paquete, y no estamos seguros si vamos a utilizar funciones de dicho paquete.

Enlaces