En esta sección, vamos a hablar sobre sentencias de control de flujo y operaciones con funciones en Go.
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
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")
}
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
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)
}
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
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 llamadafuncName
. - 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
youtput2
, 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.
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
}
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)
}
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
ymapas
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 unsegmento
, vas a necesitar pasar el puntero de forma explicita)
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)
}
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.
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
}
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
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.
- Path relativo import "./model" // carga el paquete que se encuentra en el mismo directorio, yo no recomiendo utilizar esta forma.
- 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.
-
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 estoPrintf("Hola mundo")
. -
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 estof.Printf("Hola mundo")
. -
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óninit
de ese paquete, y no estamos seguros si vamos a utilizar funciones de dicho paquete.
- Índice
- Sección anterior: Principios de Go
- Siguiente sección: struct