Lección 11 · Control de flujo y funciones
defer
A veces una función tiene que hacer algo al terminar, salga por donde salga: cerrar la caja, apagar las
luces. Al terminar esta lección vas a poder dejar una llamada para el final con defer, predecir en qué
orden se ejecutan varias y con qué valores. También vas a saber leer el error que aparece cuando a un
defer le falta la llamada.
Dejar una llamada para el final
La caja de la Papelería Central se abre al empezar el día y se cierra al terminar. Con defer, el cierre
se escribe al principio, pero se ejecuta al final:
package main
import "fmt"
func main() {
fmt.Println("Caja abierta")
defer fmt.Println("Caja cerrada")
fmt.Println("Venta: 1500")
fmt.Println("Venta: 800")
}Salida:
Caja abierta Venta: 1500 Venta: 800 Caja cerrada
defer fmt.Println("Caja cerrada") está en la segunda línea de main, pero Caja cerrada sale al final.
Cuando Go llega a un defer, no ejecuta la llamada: la guarda. Sigue con las líneas siguientes y, cuando
main termina, ejecuta la llamada guardada.
defer significa “diferir”, dejar para después. La regla: defer va seguido de una llamada a una función,
y esa llamada se ejecuta justo antes de que termine la función donde está el defer. Aquí esa función es
main, así que Caja cerrada sale después de su última línea.
A defer le falta la llamada
El cierre de la caja puede ser una función propia, cerrarCaja. Si escribes su nombre sin paréntesis, el
programa no compila:
package main
import "fmt"
func cerrarCaja() {
fmt.Println("Caja cerrada")
}
func main() {
defer cerrarCaja
fmt.Println("Caja abierta")
}Salida:
./main.go:10:8: expression in defer must be function call
Lee el mensaje por partes:
./main.go:10:8apunta acerrarCaja, en la línea 10, justo después dedefer.expression in defer must be function callsignifica “la expresión de undefertiene que ser una llamada a una función”.
Sin paréntesis, cerrarCaja es la función misma, como viste en la lección
Funciones como valores y closures; no es una llamada. Para corregirlo, agrega los paréntesis:
defer cerrarCaja().
Los paréntesis de la llamada son los únicos que lleva. defer no es una función: es una palabra de Go,
como return. Si envuelves la llamada entre paréntesis, como en defer (cerrarCaja()), Go responde
expression in defer must not be parenthesized (“la expresión de un defer no puede ir entre
paréntesis”).
defer va siempre dentro de una función. Escrito fuera, entre dos funciones, Go responde
syntax error: non-declaration statement outside function body: fuera de las funciones solo van
declaraciones, como func, const o import, y defer es una instrucción.
Limpiar al salir
El uso típico de defer es la limpieza: dejar todo como estaba al terminar. cobrar abre la caja y, si el
pago no alcanza, termina antes con un return:
package main
import "fmt"
func cerrarCaja() {
fmt.Println("Caja cerrada")
}
// cobrar abre la caja, cobra una venta y cierra la caja al terminar.
func cobrar(precio, pago int) {
fmt.Println("Caja abierta")
defer cerrarCaja()
if pago < precio {
fmt.Println("Pago insuficiente")
return
}
fmt.Println("Cambio:", pago-precio)
}
func main() {
cobrar(1500, 2000)
cobrar(1500, 1000)
}Salida:
Caja abierta Cambio: 500 Caja cerrada Caja abierta Pago insuficiente Caja cerrada
cobrar tiene dos salidas. En la primera llamada, el pago alcanza: cobrar imprime el cambio y termina
en su }. En la segunda, no alcanza, y termina en el return del if. En las dos, defer cerrarCaja()
cierra la caja justo antes de que cobrar termine.
Por eso defer sirve para limpiar: escribes el cierre una vez, al lado de la apertura, y se ejecuta por
cualquier camino de salida. Sin defer, tendrías que llamar a cerrarCaja() antes de cada return, y es
fácil olvidar uno al agregar otro. En programas reales, defer cierra archivos y conexiones, y suelta lo
que la función tomó al empezar.
Un defer también se ejecuta si la función termina por un panic (en español, “pánico”): un error al
ejecutarse que detiene el programa, como llamar a una variable función que vale nil. Lo veremos en la
lección “panic y recover”.
Consejo
Escribe el defer justo después de lo que abre: fmt.Println("Caja abierta") y, en la línea siguiente,
defer cerrarCaja(). Quien lee la función ve juntas la apertura y el cierre, y no tiene que buscar el
cierre en cada return (Effective Go).
Si vienes de JavaScript, Python o Java…
En Java y en JavaScript, la limpieza va en un bloque finally, al final de un try. En Python, en un
finally o con with. Java también tiene try con recursos, que los cierra solo. Go no tiene try ni
finally: usa defer, que se escribe al lado de la apertura y no al final de la función.
Varios defer: el último va primero
Al abrir la papelería se abre la puerta, se encienden las luces y se abre la caja. Cada paso deja su
cierre con un defer:
package main
import "fmt"
func main() {
fmt.Println("Abrir la puerta")
defer fmt.Println("Cerrar la puerta")
fmt.Println("Encender las luces")
defer fmt.Println("Apagar las luces")
fmt.Println("Abrir la caja")
defer fmt.Println("Cerrar la caja")
fmt.Println("Atender a los clientes")
}Salida:
Abrir la puerta Encender las luces Abrir la caja Atender a los clientes Cerrar la caja Apagar las luces Cerrar la puerta
Los tres defer se ejecutan al terminar main, en el orden inverso al que se escribieron. El último
guardado, Cerrar la caja, es el primero en ejecutarse; el primero guardado, Cerrar la puerta, es el
último.
Go guarda las llamadas diferidas como una pila de platos: cada defer pone un plato arriba, y al final se
sacan de arriba hacia abajo. A este orden se le dice LIFO, del inglés last in, first out: “el último
que entra es el primero que sale”.
El orden inverso es justo el que necesita la limpieza. Se cierra primero lo último que se abrió: no apagas las luces con la caja abierta, ni cierras la puerta con las luces encendidas.
Los argumentos se evalúan al diferir
Una llamada diferida usa los valores que tenían sus argumentos en la línea del defer, no los del final:
package main
import "fmt"
func main() {
ventas := 0
defer fmt.Println("Ventas según el defer:", ventas)
ventas += 1500
ventas += 800
fmt.Println("Ventas del día:", ventas)
}Salida:
Ventas del día: 2300 Ventas según el defer: 0
En la línea del defer, ventas vale 0. Go calcula en ese momento los argumentos de la llamada, el texto
y 0, y los guarda junto con ella. Después, ventas llega a 2300, pero la llamada guardada ya tiene su
0. Al terminar main, imprime Ventas según el defer: 0.
La regla: al ejecutar un defer, Go evalúa (calcula) los argumentos de la llamada; lo único que deja
para el final es ejecutarla. Cambiar la variable después del defer no cambia el valor que la llamada
ya guardó.
Un closure diferido ve el valor final
Para usar el valor del final, difiere una función anónima que lea la variable. Es la forma
func() { … }() de la lección anterior, con defer delante:
package main
import "fmt"
func main() {
ventas := 0
defer func() {
fmt.Println("Total al cerrar:", ventas)
}()
ventas += 1500
ventas += 800
fmt.Println("Ventas del día:", ventas)
}Salida:
Ventas del día: 2300 Total al cerrar: 2300
La llamada diferida es func() { … }(), sin argumentos: en la línea del defer no hay argumentos que
calcular, y Go solo guarda la función anónima. La función anónima es un closure que captura ventas,
la misma variable y no una copia. Lee
su valor solo cuando se ejecuta, al terminar main, y para entonces vale 2300.
Fíjate en el () después de la }. Sin él, defer recibe una función sin llamar, y Go responde el
mismo expression in defer must be function call de antes.
defer dentro de un for
Go llega al defer en cada vuelta, y cada vuelta guarda una llamada más:
package main
import "fmt"
func main() {
for turno := 1; turno <= 3; turno++ {
fmt.Println("Empieza el turno", turno)
defer fmt.Println("Termina el turno", turno)
}
fmt.Println("Fin del día")
}Salida:
Empieza el turno 1 Empieza el turno 2 Empieza el turno 3 Fin del día Termina el turno 3 Termina el turno 2 Termina el turno 1
Las llamadas diferidas no se ejecutan al terminar cada vuelta: se acumulan. Las tres esperan a que termine
main, después de Fin del día, y salen en orden inverso. Cada una imprime el turno de su vuelta,
porque Go evaluó el argumento en la línea del defer.
Un defer pertenece a la función donde está, no al bloque. Dentro de un for o de un if, la llamada
igual espera a que termine la función entera.
Si necesitas cerrar algo al final de cada vuelta, pon el cuerpo de la vuelta en una función. Así, su
defer se ejecuta cada vez que esa función termina:
package main
import "fmt"
// atenderTurno atiende un turno y lo cierra al terminar.
func atenderTurno(turno int) {
fmt.Println("Empieza el turno", turno)
defer fmt.Println("Termina el turno", turno)
fmt.Println("Atendiendo clientes")
}
func main() {
for turno := 1; turno <= 2; turno++ {
atenderTurno(turno)
}
fmt.Println("Fin del día")
}Salida:
Empieza el turno 1 Atendiendo clientes Termina el turno 1 Empieza el turno 2 Atendiendo clientes Termina el turno 2 Fin del día
Cada llamada a atenderTurno termina dentro de su vuelta, y su defer se ejecuta en ese momento. Un
for que abre algo en cada vuelta, con un defer adentro, deja todo abierto hasta el final de la función:
con muchas vueltas, son muchas cosas abiertas a la vez.
defer y los resultados con nombre
Un closure diferido puede cambiar lo que devuelve la función, si sus resultados tienen nombre:
package main
import "fmt"
func conNombre() (registro string) {
registro = "Venta: 1500."
defer func() {
registro += " Caja cerrada."
}()
return registro
}
func sinNombre() string {
registro := "Venta: 1500."
defer func() {
registro += " Caja cerrada."
}()
return registro
}
func main() {
fmt.Println(conNombre())
fmt.Println(sinNombre())
}Salida:
Venta: 1500. Caja cerrada. Venta: 1500.
Como viste en Conversiones de tipo, += agrega un texto al final de un string:
registro += " Caja cerrada." deja "Venta: 1500. Caja cerrada.".
Las dos funciones parecen iguales, pero devuelven textos distintos. La diferencia está en el orden en que
termina una función con un return:
- El
returnle da su valor al resultado. - Se ejecutan las llamadas diferidas.
- La función devuelve el resultado a quien la llamó.
En conNombre, el resultado es la variable registro. El return le da su valor, "Venta: 1500.", y
después el closure diferido le agrega " Caja cerrada.". Como es la misma variable, la función devuelve
el texto completo.
En sinNombre, el resultado no tiene nombre: registro es una variable común. El return copia su
valor, "Venta: 1500.", en el resultado. El closure cambia registro después, pero el resultado ya tiene
su copia, y no cambia.
Con resultados con nombre, un defer puede retocar el resultado por cualquier camino de salida, como
agregar el mismo texto al final después de cualquier return. El closure lo cambia asignándole a la
variable: si la función diferida devolviera un valor con su propio return, Go lo descartaría.
Consejo
Úsalo poco. Casi siempre se lee mejor un return con el valor final escrito. Cambiar un resultado desde
un defer vale la pena cuando hay muchos return y todos necesitan el mismo retoque, como agregar
información a un error antes de devolverlo.
En resumen
deferseguido de una llamada la guarda y la ejecuta justo antes de que termine la función, por cualquier camino de salida. Sin la llamada, Go respondeexpression in defer must be function call.- Su uso típico es la limpieza: el cierre se escribe al lado de la apertura y se ejecuta siempre.
- Varios
deferse ejecutan en orden inverso, el último primero (LIFO). - Los argumentos se evalúan en la línea del
defer; un closure diferido,defer func() { … }(), ve el valor final de las variables que captura. - Un
deferdentro de unforespera al final de la función, no al de la vuelta. Con resultados con nombre, un closure diferido puede cambiar lo que devuelve la función.
Ejercicios
Con un defer en el lugar del comentario, haz que el programa imprima Caja cerrada en una última línea, después de las dos ventas.
Para salir del editor con el teclado, pulsa Escape y luego Tab.
package main
import "fmt"
func main() {
fmt.Println("Caja abierta")
// Escribe aquí, con defer, la línea que imprime Caja cerrada.
fmt.Println("Venta: 1500")
fmt.Println("Venta: 800")
}