Manejo de errores en Golang

A diferencia de los métodos convencionales en otros lenguajes de programación convencionales como JavaScript (que utiliza la sentencia try… catch) o Python (con su bloque try… except) abordar los errores en Go requiere un enfoque diferente. ¿Por qué? Porque sus características para el manejo de errores son a menudo mal aplicadas.

En esta entrada del blog, vamos a echar un vistazo a las mejores prácticas que se podrían utilizar para manejar los errores en una aplicación Go. Una comprensión básica de cómo funciona Go es todo lo que se requiere para digerir este artículo – si usted se siente atascado en algún momento, está bien tomar algún tiempo e investigar los conceptos no familiares.

El identificador en blanco

El identificador en blanco es un marcador de posición anónimo. Se puede utilizar como cualquier otro identificador en una declaración, pero no introduce un enlace. El identificador en blanco proporciona una manera de ignorar los valores de la izquierda en una asignación y evitar errores del compilador sobre las importaciones y las variables no utilizadas en un programa. La práctica de asignar errores al identificador en blanco en lugar de manejarlos adecuadamente es insegura ya que esto significa que usted ha decidido ignorar explícitamente el valor de la función definida.

result, _ := iterate(x,y)if value > 0 { // ensure you check for errors before results.}

La razón para hacer esto es probablemente que usted no está esperando un error de la función (o cualquier error que pueda ocurrir) pero esto podría crear efectos en cascada en su programa. Lo mejor es manejar un error siempre que puedas.

Manejar errores a través de múltiples valores de retorno

Una forma de manejar errores es aprovechar el hecho de que las funciones en Go soportan múltiples valores de retorno. Así, puedes pasar una variable de error junto al resultado de la función que estás definiendo:

func iterate(x, y int) (int, error) {}

En el ejemplo de código anterior, tenemos que devolver la variable predefinida error si creemos que existe la posibilidad de que nuestra función falle. error es un tipo de interfaz declarado en el paquete built-in de Go y su valor cero es nil.

type error interface { Error() string }

Por lo general, devolver un error significa que hay un problema y devolver nil significa que no hubo errores:

result, err := iterate(x, y) if err != nil { // handle the error appropriately } else { // you're good to go }

Por lo tanto, cada vez que se llama a la función iterate y err no es igual a nil, el error devuelto debe ser manejado apropiadamente – una opción podría ser crear una instancia de un mecanismo de reintento o limpieza. El único inconveniente con el manejo de errores de esta manera es que no hay ninguna aplicación del compilador de Go, usted tiene que decidir sobre cómo la función que creó devuelve el error. Puedes definir una estructura de error y colocarla en la posición de los valores devueltos. Una forma de hacerlo es utilizando la estructura incorporada errorString (también puedes encontrar este código en el código fuente de Go):

package errors func New(text string) error { return &errorString { text } } type errorString struct { s string } func(e * errorString) Error() string { return e.s }

En el ejemplo de código anterior, errorString incorpora un string que es devuelto por el método Error. Para crear un error personalizado, tendrás que definir tu estructura de error y utilizar conjuntos de métodos para asociar una función a tu estructura:

// Define an error structtype CustomError struct { msg string}// Create a function Error() string and associate it to the struct.func(error * CustomError) Error() string { return error.msg}// Then create an error object using MyError struct.func CustomErrorInstance() error { return &CustomError { "File type not supported" }}

El error personalizado recién creado puede entonces reestructurarse para utilizar la estructura incorporada error:

 import "errors"func CustomeErrorInstance() error { return errors.New("File type not supported")}

Una limitación de la estructura incorporada error es que no viene con trazas de pila. Esto hace que sea muy difícil localizar dónde se ha producido un error. El error podría pasar a través de un número de funciones antes de ser impreso. Para manejar esto, puede instalar el paquete pkg/errors que proporciona primitivas básicas para el manejo de errores, tales como el registro de trazas de pila, la envoltura de errores, la desenvoltura y el formateo. Para instalar este paquete, ejecute este comando en su terminal:

go get github.com/pkg/errors

Cuando necesite añadir trazas de pila o cualquier otra información que facilite la depuración a sus errores, utilice las funciones New o Errorf para proporcionar errores que registren su traza de pila. Errorf implementa la interfaz fmt.Formatter que te permite formatear tus errores usando las runas del paquete fmt (%s, %v, %+v etc):

import( "github.com/pkg/errors" "fmt")func X() error { return errors.Errorf("Could not write to file")}func customError() { return X()}func main() { fmt.Printf("Error: %+v", customError())}

Para imprimir trazas de pila en lugar de un mensaje de error simple, tienes que usar %+v en lugar de %v en el patrón de formato, y las trazas de pila se verán similares al ejemplo de código de abajo:

Error: Could not write to filemain.X /Users/raphaelugwu/Go/src/golangProject/error_handling.go:7main.customError /Users/raphaelugwu/Go/src/golangProject/error_handling.go:15main.main /Users/raphaelugwu/Go/src/golangProject/error_handling.go:19runtime.main /usr/local/opt/go/libexec/src/runtime/proc.go:192runtime.goexit /usr/local/opt/go/libexec/src/runtime/asm_amd64.s:2471

Aplazamiento, pánico y recuperación

Aunque Go no tiene excepciones, tiene un tipo de mecanismo similar conocido como «Aplazamiento, pánico y recuperación». La ideología de Go es que la adición de excepciones como la declaración try/catch/finally en JavaScript daría lugar a un código complejo y alentaría a los programadores a etiquetar demasiados errores básicos, como fallar al abrir un archivo, como excepcionales. No debe utilizar defer/panic/recover como lo haría con throw/catch/finally; sólo en casos de fallos inesperados e irrecuperables.

Defer es un mecanismo del lenguaje que pone su llamada a la función en una pila. Cada función diferida se ejecuta en orden inverso cuando la función anfitriona termina, independientemente de si se llama al pánico o no. El mecanismo de aplazamiento es muy útil para limpiar recursos:

package mainimport ( "fmt")func A() { defer fmt.Println("Keep calm!") B()}func B() { defer fmt.Println("Else...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") D()}func D() { defer fmt.Println("If it's more than 30 degrees...")}func main() { A()}

Esto compilaría como:

If it's more than 30 degrees...Turn on the air conditioner...Else...Keep calm!

Panic es una función incorporada que detiene el flujo de ejecución normal. Cuando llamas a panic en tu código, significa que has decidido que tu llamada no puede resolver el problema. Por lo tanto, panic sólo debe usarse en raros casos en los que no es seguro para tu código o para cualquiera que integre tu código continuar en ese punto. Aquí está un ejemplo de código que representa cómo panic funciona:

package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}

La muestra anterior compilaría como:

If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Then we can't save the earth!panic: Global Warming!!!goroutine 1 :main.Break() /tmp/sandbox186240156/prog.go:22 +0xe0main.C() /tmp/sandbox186240156/prog.go:18 +0xa0main.B() /tmp/sandbox186240156/prog.go:14 +0xa0main.A() /tmp/sandbox186240156/prog.go:10 +0xa0main.main() /tmp/sandbox186240156/prog.go:26 +0x20Program exited: status 2.

Como se muestra arriba, cuando panic se utiliza y no se maneja, el flujo de ejecución se detiene, todas las funciones diferidas se ejecutan en orden inverso y se imprimen las trazas de pila.

Puede utilizar la función incorporada recover para manejar panic y devolver los valores que pasan de una llamada de pánico. recover siempre debe ser llamado en una función defer de lo contrario devolverá nil:

package mainimport ( "errors" "fmt")func A() { defer fmt.Println("Then we can't save the earth!") defer func() { if x := recover(); x != nil { fmt.Printf("Panic: %+v\n", x) } }() B()}func B() { defer fmt.Println("And if it keeps getting hotter...") C()}func C() { defer fmt.Println("Turn on the air conditioner...") Break()}func Break() { defer fmt.Println("If it's more than 30 degrees...") panic(errors.New("Global Warming!!!"))}func main() { A()}

Como se puede ver en el ejemplo de código anterior, recover evita que todo el flujo de ejecución se detenga porque lanzamos en una función panic y el compilador devolvería:

If it's more than 30 degrees...Turn on the air conditioner...And if it keeps getting hotter...Panic: Global Warming!!!Then we can't save the earth!Program exited.

Para informar de un error como valor de retorno, hay que llamar a la función recover en la misma goroutine en la que se llama a la función panic, recuperar una estructura de error de la función recover y pasarla a una variable:

package mainimport ( "errors" "fmt")func saveEarth() (err error) { defer func() { if r := recover(); r != nil { err = r.(error) } }() TooLate() return}func TooLate() { A() panic(errors.New("Then there's nothing we can do"))}func A() { defer fmt.Println("If it's more than 100 degrees...")}func main() { err := saveEarth() fmt.Println(err)}

Toda función diferida se ejecutará después de una llamada a función pero antes de una sentencia de retorno. Por lo tanto, puede establecer una variable devuelta antes de que se ejecute una sentencia de retorno. El ejemplo de código anterior compilaría como:

If it's more than 100 degrees...Then there's nothing we can doProgram exited.

Envoltura de errores

Antes, la envoltura de errores en Go sólo era accesible mediante el uso de paquetes como pkg/errors. Sin embargo, con la última versión de Go – versión 1.13, el soporte para la envoltura de errores está presente. Según las notas de la versión:

Un error e puede envolver otro error w proporcionando un método Unwrap que devuelva w. Tanto e como w están disponibles para los programas, lo que permite a e proporcionar un contexto adicional a w o reinterpretarlo, al tiempo que permite a los programas tomar decisiones basadas en w.

Para crear errores envueltos, fmt.Errorf tiene ahora un verbo %w y para inspeccionar y desenvolver errores, se han añadido un par de funciones al paquete error:

errors.Unwrap: Esta función básicamente inspecciona y expone los errores subyacentes en un programa. Devuelve el resultado de llamar al método Unwrap en Err. Si el tipo de Err contiene un método Unwrap devuelve un error. En caso contrario, Unwrap devuelve nil.

package errorstype Wrapper interface{ Unwrap() error}

A continuación se muestra un ejemplo de implementación del método Unwrap:

func(e*PathError)Unwrap()error{ return e.Err}

errors.Is: Con esta función, se puede comparar un valor de error contra el valor centinela. Lo que hace que esta función sea diferente de nuestras comprobaciones de errores habituales es que en lugar de comparar el valor centinela con un error, lo compara con cada error de la cadena de errores. También implementa un método Is en un error para que un error pueda publicarse como centinela aunque no sea un valor centinela.

func Is(err, target error) bool

En la implementación básica anterior, Is comprueba e informa si err o cualquiera de los errors de su cadena son iguales al objetivo (valor centinela).

errors.As: Esta función proporciona una forma de lanzar a un tipo de error específico. Busca el primer error en la cadena de errores que coincida con el valor centinela y si lo encuentra, establece el valor centinela a ese valor de error y devuelve true:

package mainimport ( "errors" "fmt" "os")func main() { if _, err := os.Open("non-existing"); err != nil { var pathError *os.PathError if errors.As(err, &pathError) { fmt.Println("Failed at path:", pathError.Path) } else { fmt.Println(err) } }}

Puede encontrar este código en el código fuente de Go.

Resultado del compilador:

Failed at path: non-existingProgram exited.

Un error coincide con el valor centinela si el valor concreto del error es asignable al valor apuntado por el valor centinela. As entrará en pánico si el valor centinela no es un puntero no nulo a un tipo que implemente el error o a cualquier tipo de interfaz. As devuelve false si err es nil.

Resumen

La comunidad de Go ha estado dando pasos impresionantes últimamente con el soporte de varios conceptos de programación e introduciendo formas aún más concisas y fáciles de manejar errores. ¿Tienes alguna idea sobre cómo manejar o trabajar con errores que puedan aparecer en tu programa Go? Házmelo saber en los comentarios de abajo.

Recursos:
Especificación del lenguaje de programación de Go sobre Type assertion
La charla de Marcel van Lohuizen en dotGo 2019 – Go 2 error values today
Go 1.13 release notes

LogRocket: Visibilidad completa de tus aplicaciones web

LogRocket es una solución de monitorización de aplicaciones frontales que te permite reproducir los problemas como si ocurrieran en tu propio navegador. En lugar de adivinar por qué se producen los errores, o pedir a los usuarios capturas de pantalla y volcados de registro, LogRocket le permite reproducir la sesión para entender rápidamente lo que ha ido mal. Funciona perfectamente con cualquier aplicación, independientemente del marco de trabajo, y tiene plugins para registrar el contexto adicional de Redux, Vuex, y @ngrx/store.

Además de registrar las acciones y el estado de Redux, LogRocket registra los registros de la consola, los errores de JavaScript, los stacktraces, las solicitudes/respuestas de red con cabeceras + cuerpos, los metadatos del navegador, y los registros personalizados. También instrumenta el DOM para registrar el HTML y el CSS de la página, recreando vídeos de píxeles perfectos incluso de las aplicaciones más complejas de una sola página.

Pruébalo gratis.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.