Lección 10 · Control de flujo y funciones
Funciones como valores y closures
En la lección anterior escribiste funciones y las llamaste por su nombre. En Go, una función también es un valor, como un número o un texto. Al terminar esta lección vas a poder guardar una función en una variable, pasarla a otra función, devolverla y escribir funciones sin nombre que recuerdan variables: los closures. También vas a saber leer los errores que aparecen cuando una función no es del tipo que se espera o cuando falta llamarla.
Una función es un valor
La Papelería Central tiene dos ofertas: la mitad del precio, o 300 menos. Las dos son funciones, y una variable puede guardar cualquiera de las dos:
package main
import "fmt"
func mitad(precio int) int {
return precio / 2
}
func menos300(precio int) int {
return precio - 300
}
func main() {
descuento := mitad
fmt.Println(descuento(1500))
descuento = menos300
fmt.Println(descuento(1500))
fmt.Printf("%T\n", descuento)
}Salida:
750 1200 func(int) int
descuento := mitad no llama a mitad: no hay paréntesis. Sin paréntesis, mitad es la función misma, y
:= la guarda en la variable descuento. Con paréntesis, mitad(1500) es una llamada, y vale lo que
devuelve.
descuento(1500) llama a la función que guarda descuento. Primero guarda mitad, así que da 750.
Después, descuento = menos300 le da otra función, y la misma llamada da 1200.
%T muestra el tipo de descuento: func(int) int, una función que recibe un int y devuelve un int.
Como el de toda variable, ese tipo queda fijo: descuento solo puede guardar funciones que reciben un int
y devuelven un int.
Si imprimes la variable sin llamarla, con fmt.Println(descuento), no ves 750. Ves un número como
0xe5ad380: el lugar de la función en la memoria de la computadora. Si ves algo así, faltan los
paréntesis de la llamada.
Solo se llama a una función
Los paréntesis después de un nombre lo llaman. Si el nombre no guarda una función, el programa no
compila. Aquí, descuento guarda un monto, no una función:
package main
import "fmt"
func main() {
descuento := 500
fmt.Println(descuento(1500))
}Salida:
./main.go:7:14: invalid operation: cannot call descuento (variable of type int): int is not a function
Lee el mensaje por partes:
./main.go:7:14apunta adescuento(1500), en la línea 7.invalid operation: cannot call descuentosignifica “operación no válida: no se puede llamar adescuento”.(variable of type int)dice qué esdescuento: una variable de tipoint.int is not a function: unintno es una función.
Si descuento tiene que ser un monto, réstalo: 1500 - descuento. Si tiene que ser una función, guarda
una en la variable, como en descuento := mitad.
Tipos función
Una función puede recibir otra función como parámetro. aplicar recibe un precio y la oferta que hay que
aplicarle:
package main
import "fmt"
// aplicar devuelve el precio después de pasarlo por descuento.
func aplicar(precio int, descuento func(int) int) int {
return descuento(precio)
}
func mitad(precio int) int {
return precio / 2
}
func menos300(precio int) int {
return precio - 300
}
func main() {
fmt.Println("Cuaderno:", aplicar(1500, mitad))
fmt.Println("Mochila:", aplicar(4200, menos300))
}Salida:
Cuaderno: 750 Mochila: 3900
El parámetro descuento es de tipo func(int) int. Es un tipo función: la palabra func, los tipos
de los parámetros entre paréntesis y, después, el tipo del resultado. Algunos ejemplos:
func(): no recibe nada y no devuelve nada.func(int) int: recibe uninty devuelve unint.func(int, int) int: recibe dosinty devuelve unint.func(string) bool: recibe unstringy devuelve unbool.
aplicar no sabe qué oferta va a recibir: llama a la que le pasan. En la primera llamada, descuento es
mitad, y aplicar devuelve 750. En la segunda es menos300, y devuelve 3900. Las funciones se pasan
por su nombre, sin paréntesis: aplicar(1500, mitad).
Un tipo función también puede llevar los nombres de los parámetros, como func(precio int) int. Los
nombres no cuentan: es el mismo tipo que func(int) int. Para que una función sirva donde va un tipo
función, tiene que recibir los mismos tipos, en el mismo orden, y devolver el mismo tipo.
Una función de otro tipo
Esta mitad devuelve un float64, con decimales. Recibe un int, como pide aplicar, pero no devuelve
un int:
package main
import "fmt"
func aplicar(precio int, descuento func(int) int) int {
return descuento(precio)
}
func mitad(precio int) float64 {
return float64(precio) / 2
}
func main() {
fmt.Println(aplicar(1500, mitad))
}Salida:
./main.go:14:28: cannot use mitad (value of type func(precio int) float64) as func(int) int value in argument to aplicar
Es el mensaje cannot use que ya conoces (“no se puede usar”), ahora con tipos función:
mitad (value of type func(precio int) float64)dice qué esmitad: una función que recibe uninty devuelve unfloat64. Go escribe el nombre del parámetro,precio, pero no cuenta.as func(int) int valuedice qué se esperaba: una función que devuelve unint.in argument to aplicardice dónde: en un argumento de la llamada aaplicar.
Para corregirlo, haz que mitad devuelva un int: func mitad(precio int) int, con
return precio / 2.
Otro error parecido es pasar lo que devuelve la función en vez de la función: con
aplicar(1500, mitad(1500)), Go responde cannot use mitad(1500) (value of type int). mitad(1500) ya es
una llamada, y vale 750, un int. Pasa la función sin paréntesis, y aplicar la llama.
Si vienes de JavaScript, Python o Java…
En JavaScript y en Python también pasas funciones como valores, pero sin escribir su tipo: cualquier
función entra en el parámetro, y el problema aparece al ejecutar. En Java, un parámetro así es una interfaz,
como Function<Integer, Integer>, y le pasas una referencia como Papeleria::mitad. En Go escribes el
tipo función en la firma, func(int) int, y el compilador revisa cada llamada.
Funciones anónimas
Una función también se puede escribir sin nombre, justo donde se usa:
package main
import "fmt"
func aplicar(precio int, descuento func(int) int) int {
return descuento(precio)
}
func main() {
menos10PorCiento := func(precio int) int {
return precio - precio/10
}
fmt.Println("Regla:", aplicar(800, menos10PorCiento))
fmt.Println("Lápiz:", aplicar(500, func(precio int) int {
return precio - 100
}))
}Salida:
Regla: 720 Lápiz: 400
func(precio int) int { … }, sin nombre después de func, es una función anónima. Tiene parámetros,
resultado y cuerpo, como cualquier función, pero es un valor: va donde va un valor. Como en toda función,
la { va al final de su primera línea.
- La primera se guarda en la variable
menos10PorCientocon:=, y después se pasa aaplicar.800menos su décima parte,80, da720. - La segunda se escribe directamente como argumento de
aplicar. Su}cierra la función anónima, y los dos)que siguen cierranaplicar(yfmt.Println(. Da500 - 100, que es400.
Una función anónima se puede escribir dentro de main o de otra función. Una función con nombre, no: va
siempre afuera, como viste en la lección Funciones.
Como toda variable, una que guarda una función tiene que usarse. Si nunca la llamas ni la pasas, Go
responde declared and not used: menos10PorCiento.
Una función anónima suelta
Escrita sola en su línea, una función anónima no hace nada, y el programa no compila:
package main
import "fmt"
func main() {
func() {
fmt.Println("Bienvenido a la Papelería Central")
}
fmt.Println("¿Qué vas a llevar?")
}Salida:
./main.go:6:2: func() {…} (value of type func()) is not usedfunc() {…} es la función anónima; Go abrevia su cuerpo con {…}. (value of type func()) dice que es un
valor de tipo func(), y is not used, que no se usa: nadie la llama ni la guarda.
Para usarla después, guárdala en una variable, como bienvenida := func() { … }, y llámala con
bienvenida(). Si querías ejecutarla en ese mismo lugar, agrega () después de su }: func() { … }()
la llama en el momento. Sola, esa forma se usa poco; es común junto con defer, el tema de la lección
siguiente.
Consejo
Si la función es corta y la usas una sola vez, escríbela anónima, en el lugar donde la usas. Si es larga o
la usas en varios lugares, dale un nombre: se lee mejor aplicar(1500, mitad) que diez líneas dentro de
una llamada.
Closures
Una función anónima puede usar las variables del lugar donde se escribe:
package main
import "fmt"
func main() {
vendidos := 0
vender := func(cantidad int) {
vendidos += cantidad
}
vender(3)
vender(2)
fmt.Println("Cuadernos vendidos:", vendidos)
}Salida:
Cuadernos vendidos: 5
vendidos está declarada en main, fuera de la función anónima, y la función la usa igual. No recibe una
copia: usa la misma variable. Por eso cada llamada a vender cambia el vendidos de main, y al final
vale 3 + 2, que es 5.
Una función que usa variables de afuera es un closure (en español, “clausura”, pero en Go se dice
closure). Se dice que la función captura esas variables: vender captura vendidos.
Funciones que devuelven funciones
Una función también puede devolver otra. descuentoFijo arma una oferta de un monto fijo:
package main
import "fmt"
// descuentoFijo devuelve una función que le resta monto a un precio.
func descuentoFijo(monto int) func(int) int {
return func(precio int) int {
return precio - monto
}
}
func main() {
menos300 := descuentoFijo(300)
menos500 := descuentoFijo(500)
fmt.Println("Cuaderno:", menos300(1500))
fmt.Println("Mochila:", menos500(4200))
fmt.Println("Regla:", descuentoFijo(100)(800))
}Salida:
Cuaderno: 1200 Mochila: 3700 Regla: 700
El tipo del resultado de descuentoFijo es func(int) int, y su return devuelve una función anónima de
ese tipo. Esa función es un closure: usa monto, el parámetro de descuentoFijo.
Cada llamada a descuentoFijo crea una función nueva, con su propio monto. menos300 recuerda 300, y
menos500 recuerda 500. descuentoFijo ya terminó cuando se llaman, pero su monto sigue existiendo,
porque el closure lo usa.
descuentoFijo(100)(800) hace las dos cosas en una línea. Los primeros paréntesis llaman a
descuentoFijo y dan una función; los segundos llaman a esa función con 800. Los mensajes de los
ejercicios usan esta forma: descuentoDe(20)(1500).
Un contador con closure
Un closure también puede cambiar la variable que captura, y esa variable sigue viva entre una llamada y la siguiente. Así funciona un contador de turnos para cada caja:
package main
import "fmt"
// contadorDeTurnos devuelve una función que da el turno siguiente.
func contadorDeTurnos() func() int {
turno := 0
return func() int {
turno++
return turno
}
}
func main() {
cajaUno := contadorDeTurnos()
cajaDos := contadorDeTurnos()
fmt.Println("Caja 1, turno", cajaUno())
fmt.Println("Caja 1, turno", cajaUno())
fmt.Println("Caja 2, turno", cajaDos())
fmt.Println("Caja 1, turno", cajaUno())
}Salida:
Caja 1, turno 1 Caja 1, turno 2 Caja 2, turno 1 Caja 1, turno 3
Cada llamada a contadorDeTurnos declara un turno nuevo, en 0, y devuelve un closure que lo captura.
Cada vez que llamas a ese closure, suma 1 a su turno y lo devuelve.
cajaUno y cajaDos vienen de llamadas distintas, así que cada una tiene su propio turno. Por eso
cajaDos() da 1 aunque cajaUno ya iba en 2, y la siguiente llamada a cajaUno() sigue en 3.
Ninguna otra parte del programa puede cambiar esos turno: solo el closure que los capturó. Así, un
closure guarda un dato que cambia entre llamadas, sin que nadie más pueda tocarlo.
Si vienes de JavaScript, Python o Java…
En JavaScript, los closures funcionan igual. En Python, para cambiar una variable capturada hace falta
declararla con nonlocal turno; en Go, el closure la cambia directamente. En Java, una lambda solo puede
usar variables locales que no cambian (effectively final): este contador no compila con una lambda.
En Go no hay una forma corta como (precio) => precio / 2 de JavaScript o lambda precio: precio // 2 de
Python: una función anónima se escribe siempre con func, los tipos y las llaves.
La variable del for, una por vuelta
Un closure creado dentro de un for puede usar la variable del for. La caja arma un anuncio por turno y
guarda el del primero y el del último para llamarlos al final:
package main
import "fmt"
func main() {
var primero func()
var ultimo func()
for turno := 1; turno <= 3; turno++ {
anunciar := func() {
fmt.Println("Atendiendo el turno", turno)
}
if turno == 1 {
primero = anunciar
}
ultimo = anunciar
}
primero()
ultimo()
}Salida:
Atendiendo el turno 1 Atendiendo el turno 3
var primero func() declara una variable de tipo func() sin darle valor. Su valor cero es nil, que
significa “ninguna función”. Llamarla mientras vale nil detiene el programa con un error; aquí las dos
reciben su función dentro del for, antes de llamarlas. Para saber si una variable función ya guarda una,
se compara con nil (if primero != nil), y solo con nil: descuento == mitad no compila.
En cada vuelta, anunciar es un closure nuevo, que captura turno. El de la primera vuelta se guarda en
primero. ultimo recibe el de cada vuelta, así que al final tiene el de la tercera. primero() imprime
1 y ultimo() imprime 3: cada closure recuerda el turno de su vuelta.
Funciona así porque, desde Go 1.22, cada vuelta de un for tiene su propia variable. Go crea un
turno nuevo en cada vuelta, con el valor que tenía el de la vuelta anterior, y después le aplica el paso,
turno++. Lo mismo pasa con la variable de un for con range.
Antes de Go 1.22, el for tenía una sola variable turno para todas las vueltas, y cada vuelta
cambiaba su valor. Los tres closures capturaban esa misma variable. Al terminar el for, turno valía
4, el valor con el que turno <= 3 dio false. El mismo programa imprimía Atendiendo el turno 4 dos
veces.
Era un error muy común. La corrección de entonces era copiar la variable al principio del cuerpo del for:
turno := turno declara otra variable, de esa vuelta, con el mismo nombre.
Un proyecto que declara, en su archivo go.mod, una versión de Go anterior a la 1.22 conserva el
comportamiento viejo; en este curso, siempre tienes el nuevo.
Consejo
En código escrito antes de Go 1.22 vas a ver turno := turno o i := i dentro de un for. Desde Go 1.22
esa copia sobra, y puedes quitarla
(Fixing For Loops in Go 1.22).
Si vienes de JavaScript, Python o Java…
En JavaScript pasaba lo mismo con var i en un for: una sola variable para todas las vueltas. Con
let i, cada vuelta tiene la suya, como en Go desde la 1.22. En Python, las funciones creadas dentro de un
for comparten la variable: si las llamas después del for, todas ven el valor de la última vuelta.
En resumen
- Una función es un valor: se guarda en una variable (
descuento := mitad), se pasa como argumento y se devuelve. Sin paréntesis es la función misma; con paréntesis, una llamada. - Un tipo función, como
func(int) int, dice qué recibe y qué devuelve una función. Una función solo sirve donde va un tipo función si recibe y devuelve los mismos tipos (cannot use … as func(int) int value). func(precio int) int { … }sin nombre es una función anónima: un valor que se escribe donde se usa. Sola en su línea, Go respondeis not used.- Un closure es una función que usa variables de afuera. Usa la misma variable, no una copia, y la
variable sigue viva mientras el closure exista, como el
turnode un contador. - Desde Go 1.22, cada vuelta de un
fortiene su propia variable, y cada closure creado en una vuelta recuerda el valor de esa vuelta.
Ejercicios
Escribe la función aplicar(precio int, descuento func(int) int) int, que devuelve lo que vale precio después de pasarlo por la función descuento.
Una prueba oculta llama a tu función; main no se ejecuta.
Para salir del editor con el teclado, pulsa Escape y luego Tab.
package main
import "fmt"
// Escribe aquí la función aplicar, fuera de main.
func main() {
fmt.Println("Ofertas de la Papelería Central")
}