Tutorial

Información sobre defer en Go

GoDevelopment

Introducción

Go tiene muchas de las palabras claves de flujo de control comunes que se encuentran en otros lenguajes de programación, como if, switch y for, entre otras. Una palabra clave que no tienen la mayoría de los otros lenguajes de programación es defer, y aunque es menos común, pronto verá la utilidad de esta palabra en sus programas.

Uno de los principales usos de una instrucción defer es el de limpiar recursos como archivos abiertos, conexiones de red y controladores de bases de datos. Cuando su programa termine con estos recursos, es importante cerrarlos para evitar agotar los límites del programa y permitir que otros programas accedan a esos recursos. defer aporta más claridad a nuestro código y reduce su propensión a experimentar errores mediante la conservación de las invocaciones para cerrar archivos y recursos cerca de las invocaciones abiertas.

En este articulo, aprenderá a usar de forma adecuada la instrucción defer para limpiar recursos y también verá algunos errores comunes que se cometen cuando se utiliza defer.

Qué es una instrucción defer

Una instrucción defer añade la invocación de la función después de la palabra clave defer en una pila. Todas las invocaciones de la pila en cuestión se invocan cuando regresa la función en la que se añadieron. Debido a que las invocaciones se disponen en una pila, se llaman en el orden “último en entrar” y “primero en salir”.

Veremos la forma en que defer funciona imprimiendo un texto:

main.go
package main

import "fmt"

func main() {
    defer fmt.Println("Bye")
    fmt.Println("Hi")
}

En la función main hay dos instrucciones. La primera comienza con la palabra clave defer y le sigue una afirmación print que imprime Bye. La siguiente línea imprime Hi.

Si ejecutamos el programa, veremos el siguiente resultado:

Output
Hi Bye

Observe que Hi se imprimió primero. Esto es porque cualquier instrucción precedida por la palabra clave defer no se invoca hasta el final de la función en la cual se utilizó defer.

Echaremos otro vistazo al programa y esta vez añadiremos algunos comentarios para ayudar a ilustrar lo que está sucediendo:

main.go
package main

import "fmt"

func main() {
    // defer statement is executed, and places
    // fmt.Println("Bye") on a list to be executed prior to the function returning
    defer fmt.Println("Bye")

    // The next line is executed immediately
    fmt.Println("Hi")

    // fmt.Println*("Bye") is now invoked, as we are at the end of the function scope
}

La clave para comprender defer es que cuando se ejecuta la instrucción defer, los argumentos para la función diferida se evalúan de inmediato. Cuando defer se ejecuta, dispone la instrucción después de sí en una lista para que se invoque antes del regreso de la función.

Aunque este código ilustra el orden en el cual se ejecutaría defer, no es una alternativa habitual para usarla cuando se escribe un programa de Go. Es más probable que utilicemos defer para limpiar un recurso, como el controlador de un archivo. Veremos la forma de hacer eso a continuación.

Utilizar defer para limpiar recursos

En Go, es muy común usar defer para limpiar recursos. Primero, veremos un programa que escribe una cadena en un archivo, pero no utiliza defer para gestionar la limpieza del recurso:

main.go
package main

import (
    "io"
    "log"
    "os"
)

func main() {
    if err := write("readme.txt", "This is a readme file"); err != nil {
        log.Fatal("failed to write file:", err)
    }
}

func write(fileName string, text string) error {
    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    _, err = io.WriteString(file, text)
    if err != nil {
        return err
    }
    file.Close()
    return nil
}

En este programa, existe una función llamada write que primero intentará crear un archivo. Si tiene un error, lo mostrará y cerrará la función. A continuación, intenta escribir la cadena This is a readme file en el archivo especificado. Si recibe un error, lo mostrará y cerrará la función. A continuación, la función intentará cerrar el archivo y liberar el recurso de vuelta para el sistema. Finalmente, la función muestra nil para indicar que se ejecutó sin errores.

Aunque este código funciona, hay un error sutil. Si falla la invocación de io.WriteString, la función volverá sin cerrar el archivo ni liberar el recurso de vuelta para el sistema.

Podríamos solucionar el problema añadiendo otra instrucción file.Close(), método con el cual probablemente resolvería esto en un lenguaje sin defer:

main.go
package main

import (
    "io"
    "log"
    "os"
)

func main() {
    if err := write("readme.txt", "This is a readme file"); err != nil {
        log.Fatal("failed to write file:", err)
    }
}

func write(fileName string, text string) error {
    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    _, err = io.WriteString(file, text)
    if err != nil {
        file.Close()
        return err
    }
    file.Close()
    return nil
}

Ahora, incluso si la invocación de io.WriteString falla, cerraremos el archivo de todos modos. Aunque este era un error relativamente fácil de detectar y solucionar, con una función más complicada, es posible que se haya pasado por alto.

En vez de añadir la segunda invocación a file.Close(), podemos usar una instrucción defer para garantizar que independientemente de las secciones que se tomen durante la ejecución, siempre invoquemos Close().

Aquí está la versión que utiliza la palabra clave defer:

main.go
package main

import (
    "io"
    "log"
    "os"
)

func main() {
    if err := write("readme.txt", "This is a readme file"); err != nil {
        log.Fatal("failed to write file:", err)
    }
}

func write(fileName string, text string) error {
    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    defer file.Close()
    _, err = io.WriteString(file, text)
    if err != nil {
        return err
    }
    return nil
}

Esta vez, añadimos la línea de código: defer file.Close(). Esto indica al compilador que debería ejecutar file.Close antes de cerrar la función write.

Ahora, nos hemos asegurado de que, incluso si añadimos más código y creamos otra ramificación que cierre la función en el futuro, siempre limpiaremos y cerraremos el archivo.

Sin embargo, hemos introducido un error más al añadir defer. Ya no comprobaremos el error potencial que puede mostrarse desde el método Close. Esto se debe a que cuando usamos defer no hay forma de comunicar valores de retorno a nuestra función.

En Go, se considera una práctica segura y aceptada invocar Close() más de una vez sin que esto afecte al comportamiento de su programa. Si Close() muestra un error, lo hará la primera vez que se invoque. Esto nos permite invocarlo explícitamente en la ruta de ejecución correcta de nuestra función.

Veamos cómo podemos aplicar defer a la invocación para Close y, de todas formas, notificar un error si encontramos uno.

main.go
package main

import (
    "io"
    "log"
    "os"
)

func main() {
    if err := write("readme.txt", "This is a readme file"); err != nil {
        log.Fatal("failed to write file:", err)
    }
}

func write(fileName string, text string) error {
    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    defer file.Close()
    _, err = io.WriteString(file, text)
    if err != nil {
        return err
    }

    return file.Close()
}

El único cambio en este programa es la última línea en la que mostramos file.Close(). Si la invocación a Close genera un error, este ahora se mostrará como previsto para la función de invocación. Tenga en cuenta que nuestra instrucción defer file.Close() también se ejecutará después de la instrucción return. Esto significa que file.Close() posiblemente se invoque dos veces. Aunque esto no es lo ideal, es una práctica aceptable porque no debería tener efectos colaterales en su programa.

Si, sin embargo, vemos un error previamente en la función, como cuando invocamos WriteString; la función mostrará ese error y también intentará invocar a file.Close porque se difirió. Aunque file.Close puede mostrar un error (y probablemente lo haga) también, esto ya no nos importa porque vemos un error que probablemente nos indique el problema.

Hasta ahora, vimos la forma en que podemos usar un único defer para asegurarnos de limpiar nuestros recursos correctamente. A continuación, veremos la manera en que podemos usar varias instrucciones defer para limpiar más de un recurso.

Varias instrucciones defer

Es normal que haya más de una instrucción defer en una función. Crearemos un programa que solo tenga instrucciones defer para ver qué sucede cuando introducimos varias instrucciones defer:

main.go
package main

import "fmt"

func main() {
    defer fmt.Println("one")
    defer fmt.Println("two")
    defer fmt.Println("three")
}

Si ejecutamos el programa, veremos el siguiente resultado:

Output
three two one

Observe que el orden es el opuesto al que empleamos para invocar las instrucciones defer. Esto se debe a que cada instrucción diferida que se invoca se apila sobre la anterior y luego se invoca a la inversa cuando la función sale del ámbito (Last In, First Out).

Puede tener tantas invocaciones diferidas como sea necesario en una función, pero es importante recordar que todas se invocarán en el orden opuesto en el que se ejecutaron.

Ahora que comprendemos el orden en el cual se ejecutarán varias instrucciones defer, veremos la forma de usar varias instrucciones defer para limpiar varios recursos. Crearemos un programa que abra un archivo, realice tareas de escritura en él y luego lo abra de nuevo para copiar el contenido a otro archivo.

main.go
package main

import (
    "fmt"
    "io"
    "log"
    "os"
)

func main() {
    if err := write("sample.txt", "This file contains some sample text."); err != nil {
        log.Fatal("failed to create file")
    }

    if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil {
        log.Fatal("failed to copy file: %s")
    }
}

func write(fileName string, text string) error {
    file, err := os.Create(fileName)
    if err != nil {
        return err
    }
    defer file.Close()
    _, err = io.WriteString(file, text)
    if err != nil {
        return err
    }

    return file.Close()
}

func fileCopy(source string, destination string) error {
    src, err := os.Open(source)
    if err != nil {
        return err
    }
    defer src.Close()

    dst, err := os.Create(destination)
    if err != nil {
        return err
    }
    defer dst.Close()

    n, err := io.Copy(dst, src)
    if err != nil {
        return err
    }
    fmt.Printf("Copied %d bytes from %s to %s\n", n, source, destination)

    if err := src.Close(); err != nil {
        return err
    }

    return dst.Close()
}

Añadimos una nueva función llamada fileCopy. En esta función, primero abrimos nuestro archivo de origen desde el que realizaremos la copia. Comprobaremos si se mostró un error al abrir el archivo. Si es así, aplicaremos return al error y cerraremos la función. De lo contrario, aplicaremos defer al cierre del archivo de origen que acabamos de abrir.

A continuación, crearemos un archivo de destino. De nuevo, comprobaremos si aparece un error al crear el archivo. Si esto sucede, aplicaremos return a ese error y cerraremos la función. De lo contrario, también aplicaremos defer a Close() para el archivo de destino. Ahora tenemos dos funciones defer que se invocarán cuando la función cierre su ámbito.

Ahora que ambos archivos están abiertos, aplicaremos Copy() a los datos del archivo de origen al de destino. Si esto se realiza correctamente, intentaremos cerrar ambos archivos. Si observamos un error al intentar cerrar cualquiera de los archivos, aplicaremos return al error y cerraremos el ámbito de la función.

Observe que invocamos de forma explícita a Close() para cada archivo, aunque defer también invoque a Close(). Esto es para garantizar que notifiquemos el error si hay un error al cerrar un archivo. También garantiza que si, por cualquier motivo, la función se cierra antes de tiempo con un error, por ejemplo, si no pudimos realizar una copia entre los dos archivos, cada uno de ellos intentará cerrarse de forma adecuada a partir de las invocaciones diferidas.

Conclusión

En este artículo, incorporó conocimientos sobre la instrucción defer y la forma en que puede usarse para verificar que se hayan limpiado correctamente los recursos del sistema en nuestro programa. Limpiar correctamente los recursos del sistema hará que su programa utilice menos memoria y funcione mejor. Para obtener más información acerca de las aplicaciones de defer, lea el artículo sobre el manejo de Panics o consulte nuestra serie Cómo realizar codifcaciones en Go.

Creative Commons License