517 lines
20 KiB
Markdown
517 lines
20 KiB
Markdown
# 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
|
||
|
||
- [Índice](preface.md)
|
||
- Sección anterior: [Principios de Go](02.2.md)
|
||
- Siguiente sección: [struct](02.4.md)
|