Tutorial

Cómo manejar panics en Go

GoDevelopment

Introducción

Los errores que un programa encuentra se dividen en dos categorías generales: los que el programador anticipa y los que no anticipa. La interfaz error que abarcamos en nuestros dos artículos anteriores sobre el manejo de errores se encarga principalmente de los errores que esperamos a medida que escribimos programas Go. La interfaz error incluso nos permite reconocer la atípica posibilidad de que se produzca un error debido a las invocaciones de la función para que podamos responder de forma apropiada en esas situaciones.

Los “panics” corresponden a la segunda categoría de errores; es decir, la de aquellos no anticipados por el programador. Estos errores imprevistos hacen que el programa finalice de forma espontánea y que se cierre el programa Go en ejecución. Las equivocaciones comunes a menudo generan panics. A lo largo de este tutorial, examinaremos varias formas en que las operaciones comunes pueden producir panics en Go y también veremos formas de evitarlos. También usaremos instrucciones defer junto con la función recover para captar “panic” antes de que puedan cerrar inesperadamente nuestros programas de Go en ejecución.

Comprender los panics

Existen ciertas operaciones en Go que muestran panics y detienen el programa de forma automática. Entre las operaciones comunes incluyen se incluyen las de exceder la capacidad de indexación de una matriz, realizar afirmaciones de tipo, invocar métodos en punteros nulos, usar mutexes de forma incorrecta e intentar trabajar con canales cerrados. La mayoría de estas situaciones se deben a equivocaciones que se cometen durante la programación y que el compilador no puede detectar mientras compila su programa.

Debido a que entre los panics se incluyen detalles que son útiles para resolver un problema, los desarrolladores normalmente utilizan los panics como una indicación de que cometieron un error durante el desarrollo de un programa.

Panics fuera de los límites

Cuando intente acceder a un índice más allá de la extensión de un segmento o de la capacidad de una matriz, el tiempo de ejecución de Go generará un panic.

En el siguiente ejemplo se comete la equivocación común de intentar acceder al último elemento de un segmento usando la extensión del segmento que el builtin len muestra. Intente ejecutar este código para ver por la razón por la cual esto puede producir un panic:

package main

import (
    "fmt"
)

func main() {
    names := []string{
        "lobster",
        "sea urchin",
        "sea cucumber",
    }
    fmt.Println("My favorite sea creature is:", names[len(names)])
}

Esto generará el siguiente resultado:

Output
panic: runtime error: index out of range [3] with length 3 goroutine 1 [running]: main.main() /tmp/sandbox879828148/prog.go:13 +0x20

El nombre del resultado del panic proporciona una pista: panic: runtime error: index out of range. Creamos un segmento con tres criaturas marinas. Luego, intentamos obtener el último elemento del segmento indexando ese segmento con su extensión, a través de la función builtin len. Recuerde que los segmentos y las matrices se basan en cero, de modo que el primer elemento sea cero y el último elemento de este segmento se encuentre en el índice 2. Debido a que intentamos acceder al segmento en el tercer índice, 3, no hay ningún elemento en el segmento que se pueda mostrar porque está más allá de los límites de este. El tiempo de ejecución no tiene otra opción que finalizar y salir, ya que le pedimos que hiciera algo imposible. Go tampoco puede demostrar durante la compilación que este código intentará hacer esto, de modo que el compilador no puede captarlo.

Observe también que el código posterior no se ejecutó. Esto se debe a que un panic es un evento que detiene completamente la ejecución de su programa Go. El mensaje producido contiene varios datos útiles para diagnosticar la causa del panic.

Anatomía de un panic

Los panics constan de un mensaje que indica la causa que los originó y un seguimiento de pila que le permite a localizar la parte del código en la que se produjo el panic.

La primera parte de cualquier panic es el mensaje. Siempre empezará con la cadena panic: y a esta le seguirá una cadena que variará según la causa del panic. El panic del ejercicio anterior tiene el siguiente mensaje:

panic: runtime error: index out of range [3] with length 3

La cadena runtime error: situada después del prefijo panic: indica que el panic se generó a través del tiempo de ejecución del lenguaje. Este panic nos dice que intentamos usar un índice [3] que estaba fuera del rango de la extensión 3 del segmento.

Después de este mensaje se encuentra el seguimiento de pila. Los seguimientos de pila forman un mapa que podemos seguir para determinar con exactitud la línea de código que estaba en ejecución cuando se generó el panic y la forma en que un código anterior invocó ese código.

goroutine 1 [running]:
main.main()
    /tmp/sandbox879828148/prog.go:13 +0x20

Este seguimiento de pila, del ejemplo anterior, muestra que nuestro programa generó el panic desde el archivo /tmp/sandbox879828148/prog.go en la línea número 13. También nos dice que este panic se generó en la función main() desde el paquete main.

El seguimiento de pila se divide en bloques separados, uno para cada goroutine en su programa. Cada ejecución del programa de Go se concreta mediante una o más goroutines que pueden ejecutar de forma independiente y simultánea partes de su código de Go. Cada bloque comienza con el encabezado goroutine X [state]:. El encabezado le proporciona el número de ID de la goroutine junto con el estado en que estaba cuando se produjo el panic. Después del encabezado, el seguimiento de pila muestra la función que el programa ejecutaba cuando se produjo el panic, junto con el nombre de archivo y el número de línea en los que se ejecutaba la función.

El panic del ejemplo anterior se generó a través de un acceso fuera de límites a un segmento. También se pueden generar panics cuando se invocan métodos en punteros que no están establecidos.

Receptores nil

El lenguaje de programación Go tiene punteros para referirse a una instancia específica de algún tipo existente en la memoria del equipo en el tiempo de ejecución. Los punteros pueden asumir el valor nil para indicar que no apuntan a nada. Cuando intentamos invocar métodos en un puntero que tenga el valor nil, el tiempo de ejecución de Go generará un panic. De forma similar, las variables que son tipos de interfaz también producirán panics cuando se invoquen métodos en ellas. Para ver los panics generados en estos casos, pruebe con el siguiente ejemplo:

package main

import (
    "fmt"
)

type Shark struct {
    Name string
}

func (s *Shark) SayHello() {
    fmt.Println("Hi! My name is", s.Name)
}

func main() {
    s := &Shark{"Sammy"}
    s = nil
    s.SayHello()
}

Los panics producidos tendrán este aspecto:

Output
panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0xffffffff addr=0x0 pc=0xdfeba] goroutine 1 [running]: main.(*Shark).SayHello(...) /tmp/sandbox160713813/prog.go:12 main.main() /tmp/sandbox160713813/prog.go:18 +0x1a

En este ejemplo, definimos una estructura llamada Shark. Shark tiene en su receptor de puntero un método definido llamado SayHello que imprimirá un saludo de forma estándar cuando se invoque. En el cuerpo de nuestra función main, creamos una nueva instancia de esta estructura Shark y solicitamos un puntero a ella usando el operador &. Este puntero se asigna a la variable s. Luego, volvemos a asignar la variable s al valor nil con la instrucción s = nil. Finalmente, intentamos invocar el método SayHello en la variable s. En vez de recibir un mensaje favorable de Sammy, recibimos un panic que indica que hemos intentando acceder a una dirección no válida de la memoria. Debido a que la variable s es nil, cuando se invoca la función SayHello, intenta acceder al campo Name en el tipo *Shark. Debido a que éste es un receptor de puntero, y el receptor en este caso es nil, se produce un panic porque no puede eliminar la referencia de un puntero nil.

Aunque fijamos s en nil de forma explícita en este ejemplo, en la práctica esto sucede de forma menos obvia. Cuando vea panics relacionados con nil pointer dereference, asegúrese de haber asignado adecuadamente cualquier variable de puntero que pueda haber creado.

Los panics generados a partir de los punteros nil y los accesos fuera de límites son dos panics comunes que genera el tiempo de ejecución. También es posible generar un panic manualmente usando una función “builtin”.

Usar la función bultin panic

También podemos generar panics propios usando la función integrada panic. Requiere una cadena única como argumento, que es el mensaje que el panic producirá. Normalmente, este mensaje es menos detallado que la reescritura de nuestro código para mostrar un error. Además, podemos usar esto en nuestros propios paquetes para indicar a los desarrolladores que pueden haber cometido un error al utilizar el código de nuestro paquete. Siempre que sea posible, la práctica recomendada es intentar mostrar valores error a los consumidores de nuestro paquete.

Ejecute este código para ver un panic generado a partir de una función invocada desde otra función:

package main

func main() {
    foo()
}

func foo() {
    panic("oh no!")
}

El resultado del panic tiene este aspecto:

Output
panic: oh no! goroutine 1 [running]: main.foo(...) /tmp/sandbox494710869/prog.go:8 main.main() /tmp/sandbox494710869/prog.go:4 +0x40

Aquí definimos una función foo que invoca el builtin panic con la cadena "oh no!". Esta función se invoca mediante nuestra función main. Observe que el resultado contiene el mensaje panic: oh no! y el seguimiento de pila muestra una “goroutine” única con dos líneas en el seguimiento de pila: una para la función main() y una para nuestra función foo().

Vimos que los panics aparecen para cerrar nuestro programa cuando se generan. Esto puede generar problemas cuando existen recursos abiertos que deben cerrarse correctamente. Go proporciona un mecanismo para ejecutar código siempre, incluso cuando aparece un panic.

Funciones diferidas

Su programa puede tener recursos que deben limpiarse adecuadamente, incluso mientras el tiempo de ejecución procesa un panic. Go le permite diferir la ejecución de la invocación de una función hasta que la función de invocación de esta complete la ejecución. Las funciones diferidas se ejecutan incluso en presencia de un panic y se usan como mecanismo de seguridad para brindar protección contra la naturaleza caótica de los panics. Las funciones se difieren invocándolas de la forma habitual y añadiendo luego un prefijo a toda la instrucción con la palabra clave defer, como en defer sayHello(). Ejecute este ejemplo para ver cómo se puede imprimir un mensaje aunque se haya producido un panic:

package main

import "fmt"

func main() {
    defer func() {
        fmt.Println("hello from the deferred function!")
    }()

    panic("oh no!")
}

El resultado producido a partir de este ejemplo tendrá este aspecto:

Output
hello from the deferred function! panic: oh no! goroutine 1 [running]: main.main() /Users/gopherguides/learn/src/github.com/gopherguides/learn//handle-panics/src/main.go:10 +0x55

En la función main de este ejemplo, primero aplicamos defer a una invocación de una función anónima que imprime el mensaje "hello from the deferred function!". La función main produce inmediatamente un panic usando la función panic. En el resultado de este programa, primero vemos que la función diferida se ejecuta e imprime su mensaje. Después de esto, se encuentra el panic que generamos en main.

Las funciones diferidas proporcionan protección contra la naturaleza sorprendente de los panics. En las funciones diferidas, Go también nos brinda la oportunidad de impedir que un panic cierre nuestro programa Go usando otra función builtin.

Manejar los panics

Los panics tienen un único mecanismo de recuperación: la función builtin recover. Esta función le permite interceptar un panic en su camino a la pila de invocación y evitar que cierre de forma inesperada su programa. Sus reglas de uso son estrictas, pero puede ser muy valiosa en una aplicación de producción.

Ya que es parte del paquete builtin, recover puede invocarse sin importar paquetes adicionales:

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    return a / b
}

El resultado de este ejemplo será el siguiente:

Output
2009/11/10 23:00:00 panic occurred: runtime error: integer divide by zero we survived dividing by zero!

Nuestra función main en este ejemplo invoca una función que definimos: divideByZero. En esta función, aplicamos defer a una invocación de una función anónima responsable de manejar los panics que puedan surgir mientras se ejecuta divideByZero. En esta función diferida anónima, invocamos la función builtin recover y asignamos a una variable el error que muestra. Si divideByZero produce panics, se establecerá este valor error; de lo contrario, será nil. Al comparar la variable err contra nil, podemos detectar si se produjo un panic y, en este caso, registraremos el panic usando la función log.PrintIn, como si fuese cualquier otro error.

Después de esta función anónima diferida, invocamos otra función que definimos, divide, e intentamos imprimir sus resultados usando fmt.PrintIn. Los argumentos proporcionados harán que divide realice una división por cero, lo que producirá un panic.

En el resultado de este ejemplo, primero vemos el mensaje de registro de la función anónima que recupera el panic, seguido del mensaje we survived dividing by zero!. Hicimos esto gracias a la función builtin recover e impedimos un panic catastrófico que cerraría nuestro programa de Go.

El valor err mostrado por recover() es exactamente el valor que se proporcionó a la invocación de panic(). Por lo tanto, es fundamental asegurarse de que el valor err sea solo nil cuando no se produzca un panic.

Detectar panics con recover

La función recover depende del valor del error para determinar si un panic se produjo o no. Debido a que el argumento para la función panic es una interfaz vacía, puede ser cualquier tipo. El valor cero para cualquier tipo de interfaz, incluida la interfaz vacía, es nil. Se debe proceder con cuidado para evitar nil como argumento de panic, como se demuestra en este ejemplo:

package main

import (
    "fmt"
    "log"
)

func main() {
    divideByZero()
    fmt.Println("we survived dividing by zero!")

}

func divideByZero() {
    defer func() {
        if err := recover(); err != nil {
            log.Println("panic occurred:", err)
        }
    }()
    fmt.Println(divide(1, 0))
}

func divide(a, b int) int {
    if b == 0 {
        panic(nil)
    }
    return a / b
}

Esto dará el siguiente resultado:

Output
we survived dividing by zero!

Este ejemplo es idéntico al anterior, en el que se utiliza recover con algunas modificaciones. La función divide se modificó para comprobar si su divisor b es igual a 0. Si es así, generará un panic usando la función builtin panic con un argumento nil. El resultado, esta vez, no incluye el mensaje de log que muestra que se produjo un panic, aunque divide haya generado uno. Este comportamiento silencioso es el motivo por el cual es muy importante verificar que el argumento para la función builtin panic no sea nil.

Conclusión

Vimos varias formas en que se pueden producir panics en Go y la manera en que podemos recuperar el sistema cuando suceden usando la función builtin recover. Aunque es posible que no utilice panic, la recuperación adecuada ante panics es importante para que las aplicaciones de Go estén listas para la producción.

Puede explorar toda nuestra serie Cómo escribir código en Go.

Creative Commons License