Control con las funciones defer, panic y recover

Completado

Ahora vamos a echar un vistazo a algunos flujos de control que son exclusivos de Go: defer, panic y recover. Cada una de estas funciones tiene varios casos de uso. Aquí se examinarán los casos de uso más importantes.

Función defer

En Go, una instrucción defer pospone la ejecución de una función (incluidos los parámetros) hasta que finaliza la función que contiene la instrucción defer. Por lo general, se pospone una función cuando se quiere evitar olvidarse de tareas como cerrar un archivo o ejecutar un proceso de limpieza.

Puede aplazar tantas funciones como desee. Las instrucciones defer se ejecutan en orden inverso, de la última a la primera.

Ejecute el siguiente código de ejemplo para ver cómo funciona este patrón:

package main

import "fmt"

func main() {
    for i := 1; i <= 4; i++ {
        defer fmt.Println("deferred", -i)
        fmt.Println("regular", i)
    }
}

Esta es la salida del código:

regular 1
regular 2
regular 3
regular 4
deferred -4
deferred -3
deferred -2
deferred -1

Observe en este ejemplo que, cada vez que se ha aplazado fmt.Println("deferred", -i), se ha almacenado el valor de i y la llamada de función se ha agregado a una cola. Después de que la función main() termine de imprimir los valores regular, se ejecutan todas las llamadas aplazadas. Como puede ver, la salida de las llamadas aplazadas está en orden inverso (último en entrar, primero en salir), ya que se quitan de la cola.

Un caso de uso típico de la función defer es cerrar un archivo después de terminar de usarlo. Este es un ejemplo:

package main

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

func main() {
    newfile, error := os.Create("learnGo.txt")
    if error != nil {
        fmt.Println("Error: Could not create file.")
        return
    }
    defer newfile.Close()

    if _, error = io.WriteString(newfile, "Learning Go!"); error != nil {
	    fmt.Println("Error: Could not write to file.")
        return
    }

    newfile.Sync()
}

Después de crear o abrir un archivo, aplazará la función .Close() para evitar olvidarse de cerrar el archivo después de terminar.

Función panic

Los errores en tiempo de ejecución hacen que un programa de Go entre en estado de alarma, como intentar acceder a una matriz mediante un índice fuera de límites o anular la referencia de un puntero nulo. También puede forzar a un programa a entrar en estado de alarma.

La función integrada panic() detiene el flujo de control normal en un programa de Go. Cuando se usa una llamada a panic, las llamadas a las funciones diferidas se ejecutan con normalidad. El proceso continúa hacia arriba en la pila hasta que todas las funciones devuelven un resultado. Después, el programa se bloquea con un mensaje de registro. El mensaje incluye la información del error y un seguimiento de la pila para ayudarle a diagnosticar la causa principal del problema.

Al llamar a la función panic(), puede agregar cualquier valor como argumento. Normalmente, se envía un mensaje de error sobre el motivo de la alerta de pánico.

Por ejemplo, el código siguiente combina las funciones panic y defer. Pruebe a ejecutar este código para ver cómo se interrumpe el flujo de control. Observe que los procesos de limpieza siguen en ejecución.

package main

import "fmt"

func highlow(high int, low int) {
    if high < low {
        fmt.Println("Panic!")
        panic("highlow() low greater than high")
    }
    defer fmt.Println("Deferred: highlow(", high, ",", low, ")")
    fmt.Println("Call: highlow(", high, ",", low, ")")

    highlow(high, low + 1)
}

func main() {
    highlow(2, 0)
    fmt.Println("Program finished successfully!")
}

Este es el resultado:

Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
panic: highlow() low greater than high

goroutine 1 [running]:
main.highlow(0x2, 0x3)
	/tmp/sandbox/prog.go:13 +0x34c
main.highlow(0x2, 0x2)
	/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x1)
	/tmp/sandbox/prog.go:18 +0x298
main.highlow(0x2, 0x0)
	/tmp/sandbox/prog.go:18 +0x298
main.main()
	/tmp/sandbox/prog.go:6 +0x37

Program exited: status 2.

Esto es lo que sucede cuando se ejecuta el código:

  1. Todo se ejecuta con normalidad. El programa imprime los valores de high y low que se pasan a la función highlow().

  2. Cuando el valor de low es mayor que el valor de high, el programa entra en estado de alarma. Verá el mensaje Panic!. En este punto, se interrumpe el flujo de control y todas las funciones aplazadas empiezan a imprimir el mensaje Deferred....

  3. El programa se bloquea y verá el seguimiento de pila completo. No verá el mensaje Program finished successfully!.

Se suele ejecutar una llamada a la función panic() cuando no se esperan errores graves. Para evitar un bloqueo del programa, puede usar otra función llamada recover().

Función recover

En ocasiones, puede que quiera evitar que un programa se bloquee e informar del error internamente. O quizás quiera limpiar el desorden antes de dejar que el programa se bloquee. Por ejemplo, podría cerrar las conexiones a un recurso para evitar más problemas.

Go proporciona la función integrada recover() para que pueda recuperar el control después de un estado de alerta. Solo se llama a recover en una función en la que también se llama a defer. Si llama a la función recover(), se devuelve nil sin ningún otro efecto en la ejecución normal.

Pruebe a modificar la función main del código anterior para agregar una llamada a la función recover(), de la siguiente manera:

func main() {
    defer func() {
	handler := recover()
        if handler != nil {
            fmt.Println("main(): recover", handler)
        }
    }()

    highlow(2, 0)
    fmt.Println("Program finished successfully!")
}

Al ejecutar el programa, la salida debe parecerse a esta:

Call: highlow( 2 , 0 )
Call: highlow( 2 , 1 )
Call: highlow( 2 , 2 )
Panic!
Deferred: highlow( 2 , 2 )
Deferred: highlow( 2 , 1 )
Deferred: highlow( 2 , 0 )
main(): recover from panic highlow() low greater than high

Program exited.

¿Ve la diferencia con respecto a la versión anterior? La principal diferencia es que ya no se muestra el error de seguimiento de la pila.

En la función main(), se aplaza una función anónima en la que se llama a la función recover(). Una llamada a recover() no puede devolver nil cuando el programa emite una alerta de pánico. Aquí puede hacer algo para solucionar el problema, pero, en este caso, lo que hará será imprimir algo.

La combinación de las funciones panic y recover es la forma idiomática en la que Go controla las excepciones. Otros lenguajes de programación usan el bloque try/catch. Go prefiere el enfoque que se ha explorado aquí.

Para más información, consulte la propuesta para agregar una función try integrada en Go.