Saltar al contenido

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:

main.go
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:

main.go
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:8 apunta a cerrarCaja, en la línea 10, justo después de defer.
  • expression in defer must be function call significa “la expresión de un defer tiene 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:

main.go
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:

main.go
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:

main.go
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:

main.go
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:

main.go
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:

main.go
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:

main.go
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:

  1. El return le da su valor al resultado.
  2. Se ejecutan las llamadas diferidas.
  3. 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

  • defer seguido 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 responde expression 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 defer se 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 defer dentro de un for espera 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.

  main.go

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")
}
 

¿Ver la solución?

La solución reemplazará tu código en el editor. Antes de verla, prueba con una pista: se aprende más cuando lo resuelves tú.