Aunque aún parece que fue ayer, hace ya más de una semana de la GoRemoteFest, cuyo resumen podéis encontrar aquí, en dónde una de las charlas que más repercusión tuvo en las redes sociales (especialmente en Twitter) fue la charla sobre la sentencia 'defer' de Mat Ryer.
Y es, esa misma sentencia, la que nos ha llevado a escribir el artículo de hoy. Pues, pese a que Mat la cataloga como “la mejor funcionalidad de Go”, lo cuál puede ser discutible, lo cierto es que su uso está muy extendido en cualquier aplicación Go y resulta un pelín decepcionante que en un blog como este aún no se hubiera hablado al respecto.
Dicho lo cual, veamos en qué consiste esa sentencia y qué peculiaridades tiene.
¿Qué es la sentencia ‘defer’?
Bien, pues la sentencia defer
es una sentencia que pertenece al core del lenguaje (es decir, no hace falta importar
ningún paquete para usarla) y que se usa junto con la llamada a una función. Esta puede ser anónima o no.
Veamos algunos ejemplos:
func printHello() {
fmt.Println("Hello World")
}
func main() {
defer printHello()
}
o bien:
func main() {
defer func() {
fmt.Println("Hello World")
}()
}
o incluso directamente:
func main() {
defer fmt.Println("Hello World")
}
Y, básicamente, lo que va a hacer la sentencia defer
es marcar esa llamada a la función “deferreada” como llamada
a ejecutar justo antes de finalizar la ejecución de la función en dónde se haya ejecutado la sentencia.
Es decir, en nuestro ejemplo anterior, si tuviéramos código después de la sentencia defer
, ese código sería ejecutado
y, finalmente, se ejecutaría la función printHello
, la función anónima o la función fmt.Println
(en cada caso).
Una vez tenemos eso claro, deberíamos ser capaces de identificar que la salida del siguiente código:
func main() {
defer fmt.Println("Goodbye")
fmt.Println("Hello World")
}
será:
Hello World
Goodbye
Además, esta sentencia podrá ser usada varias veces dentro de la misma función, de forma que las llamadas a las funciones se ejecutarán en el orden inverso al que son “deferreadas”, es decir, siguiendo un algoritmo LIFO (Last In, First Out).
Es decir, el siguiente código:
func main() {
defer fmt.Println("Goodbye")
defer fmt.Println("Hello World")
}
resultará en la siguiente salida:
Hello World
Goodbye
¿Cuándo usar la sentencia ‘defer’?
Vale, ya empezamos a saber cómo funciona la sentencia defer
, pero, ¿cuándo la debemos usar? Si bien es cierto que
cualquiera de los ejemplos que vimos anteriormente son completamente válidos, la idea de usar esta sentencia es
aprovecharnos de los beneficios que conlleva, pues, como veremos más adelante, el uso de esta sentencia también tiene su
coste y, de otro modo, lo estaremos pagando sin recibir nada a cambio.
Así que, los casos en los que tendrá más sentido utilizar esta sentencia serán aquellos en los que tengamos que ejecutar
una (o varias) funciones adicionales antes de salir de la función actual de modo que la alternativa al uso del defer
sea copiar y pegar dicha llamada en los diferentes puntos de salida de la función. Veamos algunos ejemplos:
Para cerrar file descriptors
Sin entrar en detalle sobre qué es un file descriptor,
probablemente os suenen mucho (incluso de otros lenguajes) las llamadas a File.Close
o a Response.Body.Close
, que se usan, por ejemplo, a la hora
de trabajar con ficheros (las siguientes líneas de código copian el contenido de un fichero en otro):
func main() {
src, err := os.Open(srcName)
if err != nil {
log.Fatal(err)
}
dst, err := os.Create(dstName)
if err != nil {
src.Close()
log.Fatal(err)
}
written, err = io.Copy(dst, src)
dst.Close()
src.Close()
if err != nil {
log.Fatal(err)
}
}
En estos casos, el uso de la sentencia defer
nos vendrá extremadamente bien, pues el código anterior lo podríamos
refactorizar del siguiente modo:
func main() {
src, err := os.Open(srcName)
if err != nil {
log.Fatal(err)
}
defer src.Close()
dst, err := os.Create(dstName)
if err != nil {
log.Fatal(err)
}
defer dst.Close()
written, err = io.Copy(dst, src)
if err != nil {
log.Fatal(err)
}
}
Es decir, lo que nos permite hacer el defer
en estos casos es: cada vez que abrimos satisfactoriamente (err == nil
) un
fichero, “deferreamos” la llamada a la función Close
de dicho fichero de forma que nos aseguramos de qué, sea cuál sea el
punto de salida de la ejecución de la función, nunca se quedará el fichero sin cerrar. Esto nos va a permitir escribir
un código más legible y, por ende, más mantenible, pues nos evitaremos tener código duplicado que dificulta la lectura
además de eliminar la posibilidad de introducir un nuevo bug si a la hora de extender dicha función aparece un nuevo
punto de salida de la función en el cuál nos podamos olvidar de llamar a la función Close
.
Para contabilizar el tiempo de ejecución
Otro caso común de uso para esta sentencia es aquellas situaciones en las que queramos medir cuánto tiempo tardan en ejecutarse
unas cuantas líneas de código (como podría suceder en un middleware HTTP que registra métricas de duración de las llamadas
o que escribe logs con la misma). La situación ahora es similar al caso anterior, lo que en este caso, lo que vamos a hacer es
inicializar una variable con el tiempo inicial (t := time.Now()
) y luego calcular el tiempo transcurrido (time.Since(t)
). De
igual modo, si el código que queremos medir tiene varios puntos de salida, tendremos que repetir la línea correspondiente
al cálculo de la diferencia de tiempos en cada uno de esos puntos de salida. De nuevo, la sentencia defer
nos va a
simplificar el código en cuestión. Veamos el ejemplo del middleware HTTP que mencionamos anteriormente:
func(w http.ResponseWriter, r *http.Request) {
t := time.Now()
defer func() {
log.Println(time.Since(t)
})
// middleware code
}
Para recuperarnos de un panic
Como ya deberíais saber de nuestros múltiples artículos de errores, los panics en Go se pueden recuperar
mediante la sentencia recover
. Sin embargo, como la sentencia panic
corta de inmediato el hilo de ejecución, la forma
que tenemos de asegurarnos que la función recover
es ejecutada después de un panic
es precisamente mediante el uso
de la sentencia defer
. Veamos un ejemplo:
func somethingThatPanics() {
panic("something unexpected")
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered from", r)
}
}()
somethingThatPanics()
}
Además, podríamos seguir extendiendo esta lista casi de forma indefinida con casi cualquier ejemplo que se nos ocurra que tenga una necesidad similar a la de los ejemplos expuestos, por ejemplo, en el uso de locks (muy común también).
Y, ¿cuál es el coste de usar ‘defer’?
Por desgracia, como adelantamos anteriormente, el uso de la sentencia defer
no es gratuito. Aunque, como veremos a
continuación, su coste se ha reducido muchísimo a medida que se han ido publicando nuevas versiones del lenguaje.
Veamos una tabla con los costes aproximados a modo de referencia.
Para la interpretación de la misma podemos usar el coste de llamar a una función de forma regular, en cuyo caso es de 3 ns.
Versión | Coste temporal |
---|---|
v1.7 | 99 ns |
v1.8 | 90 ns |
v.12 | 50 ns |
v.13 | 35 ns |
v.14 | 4 ns |
¿Es realmente un problema?
Entonces, una vez visto el sobrecoste que tiene “deferrear” una función (incluso en versiones más antiguas), la primera pregunta que deberíamos hacernos es: ¿es realmente un problema para nuestro caso de uso? ¿o nos compensa más tener un código más legible y menos propenso a la introducción de nuevos bugs? Pues, si realmente no nos va a venir de esos pocos nanosegundos, tenemos el problema resuelto.
Open-coded defers
Sin embargo, incluso si esos nanosegundos son un problema, quizás a partir de la última versión de Go (v1.14) han dejado de serlo. Pues, esta última versión incluye lo que se conoce como open-coded defers y que se trata de una estrategia basada en el inlining de las llamadas “deferreadas” cuya mejora de rendimiento nos permite obtener resultados muy cercanos al coste de llamar a una función de forma estándar.
Así que, ¡tema resuelto!
Aunque, es importante tener en cuenta que, el beneficio de esta estrategia será válido siempre y cuándo el número de
defer
s en una función sea de ocho o menos. Ya que, para la implementación de ésta mejora se ha usado una estructura
de ocho “dirty bits” para la gestión del inlining, con lo cuál, si “deferreamos” más funciones, no nos podremos beneficiar
de ella.
Advertencias adicionales
Finalmente, como el uso de esta sentencia es algo bastante particular de Go (a diferencia de otras características del lenguaje que son compartidas, en mayor o menor medida, con otros lenguajes), queremos mostrar algunas situaciones particulares o errores comunes con el fin de proporcionar una explicación clara de cara a futuras dudas que puedan surgir.
Deferrear nil lanza un panic
En primer lugar, hay que ir con cuidado con las referencias a las funciones que vayamos a “deferrear” pues, si lo hacemos
sobre una referencia que sea nil
, obtendremos un panic
. Veamos un pequeño fragmento de código a modo de ejemplo:
func main() {
var run func() = nil
defer run()
}
// panic: runtime error: invalid memory address or nil pointer dereference
En este caso, la ejecución de la sentencia defer
no dará ningún problema. Sin embargo, cuándo la función “deferreada”
vaya a ser ejecutada (antes de salir de la función en cuestión), obtendremos un panic
, pues se estará intentando ejecutar
una función cuyo valor es nil
.
Las funciones “deferreadas” dentro de un bucle no se ejecutan hasta el final de la función
En segundo lugar, es importante recordar que el alcance (scope) de la sentencia defer
es la función, con lo cuál,
si hacemos varias llamadas a esta sentencia dentro de un bucle, tenemos que seguir recordando que las funciones “deferreadas”
no se ejecutarán hasta que no termine la ejecución de toda la función. Veamos un ejemplo bastante común:
func main() {
for {
row, err := db.Query("SELECT...")
if err != nil {
// error handling
}
defer row.Close()
// row's usage
}
}
Una posible solución para este caso podría ser, por ejemplo, encapsular el fragmento de código que usa la row
en otra
función, de forma que, desde el bucle solo se llame a esa función en cada iteración y sea esa función la que se asegure
de cerrar el file descriptor (como vimos anteriormente) mediante la sentencia defer
.
Cuidado con el uso de referencias en los argumentos de una función “deferreada”
En tercer lugar, si lo que vamos a pasar a una función “deferreada” es una referencia a una variable (y no una copia
de la misma), es importante recordar que el valor que tendrá dicha referencia será el que haya obtenido al finalizar
toda la ejecución de código hasta llegado el punto de salida de la función, y no el que tenía cuándo invocamos la
sentencia defer
. Veamos un par de ejemplos:
package main
import (
"fmt"
)
type Human struct {
Name string
}
func (h *Human) SayHello() {
fmt.Println("Hello, I'm", h.Name)
}
func main() {
h := &Human{"Pepe"}
defer h.SayHello()
h.Name = "Juan"
}
// Hello, I'm Juan
En este primer ejemplo, el uso de la referencia se explicíta en la primera línea de la función main
así como en la
definición del método SayHello
, así que debería ser más fácil de detectar. Sin embargo, el caso de los bucles,
como suele suceder con la concurrencia, es también un error muy frecuente:
func main() {
for i := 0; i < 5; i++ {
defer func() {
fmt.Println("I'm the iteration number", i)
}()
}
}
// I'm the iteration number 5
// I'm the iteration number 5
// I'm the iteration number 5
// I'm the iteration number 5
// I'm the iteration number 5
Por suerte, este tipo de “errores” puede ser detectados a día de hoy un linter como go vet
.
Es recomendable usar named returns para los retornos tratados con defer
Finalmente, no debemos olvidar que el retorno de las funciones “deferreadas” no será usado como retorno de la función
desde dónde es invocada la sentencia defer
. Veamos un ejemplo que nos lo dejará más claro:
func add(x, y int) int {
defer func() int {
return x + y
}()
return 0
}
// fmt.Println(add(2, 2)) ==> 0
Pese a que en este ejemplo, por cuestiones de sencillez, el uso de la sentencia defer
no tiene mucho sentido. Este tipo
de fragmentos de código son más habituales de lo que podéis imaginar, especialmente para la recuperación de panic
s y el
retorno los consiguientes errores.
En este caso podríamos corregir el fragmento anterior del siguiente modo:
func add(x, y int) (result int) {
defer func() {
result = x + y
}()
return 0
}
// fmt.Println(add(2, 2)) ==> 4
Es decir, hacer uso de los ya conocidos named returns.
Y, si todo esto te ha sabido a poco, te recomendamos écharle un vistazo a la propuesta (e implementación) de los open-coded
defers anteriormente enlazada, así como investigar el funcionamiento del struct _defer
, principal responsable de la gestión
interna de la sentencia defer
.
Como siempre, estaremos muy agradecidos de que nos dejéis vuestras experiencias personales, dudas o sugerencias en los comentarios o en nuestra cuenta de Twitter.