Un código sólido debe reaccionar de forma adecuada en circunstancias imprevistas, como entradas incorrectas de los usuarios o conexiones de red o discos defectuosos. El manejo de errores es el proceso de identificar cuando sus programas se encuentran en un estado imprevisto y de tomar medidas para registrar información de diagnóstico para una depuración posterior.

A diferencia de otros lenguajes que requieren que los desarrolladores manejen los errores con una sintaxis especial, los errores en Go son valores del tipo error que se devuelven de funciones como cualquier otro valor. Para manejar errores en Go, debemos examinar los errores que pueden devolver las funciones, determinar si se produjo un error y tomar las medidas adecuadas para proteger los datos e informarles a los usuarios y los operadores que se produjo un error.

Creación de errores

Para poder manejar errores, debemos crear algunos primero. La biblioteca estándar ofrece dos funciones incorporadas para crear errores: errors.New y fmt.Errorf. Estas dos funciones le permiten especificar un mensaje de error personalizado para presentarlo, posteriormente, a sus usuarios.

errors.New tiene un solo argumento: un mensaje de error con una cadena que puede personalizar para avisarles a sus usuarios cuál fue el problema.

Intente ejecutar el ejemplo que se indica a continuación para ver un error creado por errors.New incorporado a una salida estándar:

package main

import (
    "errors"
    "fmt"
)

func main() {
    err := errors.New("barnacles")
    fmt.Println("Sammy says:", err)
}
Output
Sammy says: barnacles

Usamos la función errors.New de la biblioteca estándar para crear un nuevo mensaje de error con la cadena "barnacles" como mensaje de error. Aquí, seguimos la convención al usar minúsculas para el mensaje de error, tal como se sugiere en la Guía de Estilo del Lenguaje de Programación Go.

Por último, usamos la función fmt.Println para combinar nuestro mensaje de error con "Sammy says:".

La función fmt.Errorf le permite crear un mensaje de error de forma dinámica. Su primer argumento es una cadena que contiene su mensaje de error con valores de marcadores de posición, como %s para cadenas y %d para enteros. fmt.Errorf interpola los argumentos que siguen esta cadena de formato en esos marcadores de posición en orden:

package main

import (
    "fmt"
    "time"
)

func main() {
    err := fmt.Errorf("error occurred at: %v", time.Now())
    fmt.Println("An error happened:", err)
}
Output
An error happened: Error occurred at: 2019-07-11 16:52:42.532621 -0400 EDT m=+0.000137103

Usamos la función fmt.Errorf para crear un mensaje de error que incluya la hora actual. La cadena de formato que proporcionamos a fmt.Errorf contiene la directiva de formato %v que le indica a fmt.Errorf que use el formato predeterminado para el primer argumento proporcionado después de la cadena de formato. Ese argumento será la hora actual, que se proporciona mediante la función time.Now de la biblioteca estándar. De manera similar al ejemplo anterior, combinamos nuestro mensaje de error con un prefijo corto y enviamos el resultado a la salida estándar utilizando la función fmt.Println.

Manejo de errores

En general, no vería un error creado como este para utilizarlo de inmediato con ningún otro propósito, como en el ejemplo anterior. En la práctica, es mucho más frecuente crear un error y devolverlo de una función cuando ocurre un problema. Entonces, quienes invoquen esa función, utilizarán una instrucción if para ver si el error ocurrió o nil, un valor sin inicializar.

El ejemplo siguiente incluye una función que siempre devuelve un error. Cuando ejecute el programa, observe que produce la misma salida que el ejemplo anterior, a pesar de que, ahora, el error se está devolviendo de una función. La declaración de un error en una ubicación distinta no modifica el mensaje del error.

package main

import (
    "errors"
    "fmt"
)

func boom() error {
    return errors.New("barnacles")
}

func main() {
    err := boom()

    if err != nil {
        fmt.Println("An error occurred:", err)
        return
    }
    fmt.Println("Anchors away!")
}
Output
An error occurred: barnacles

Aquí, definimos una función denominada boom() que devuelve un único error que creamos utilizando errors.New. Luego, llamamos a esta función y captamos el error con la línea err := boom(). Una vez asignemos este error, comprobaremos si estaba presente con el condicional if err ! = nil. Aquí, el condicional siempre evaluará a true, dado que siempre estamos devolviendo un error de boom().

Esto no siempre será así, por lo que es recomendable contar con casos lógicos de manipulación en los que el error no esté presente (nil) y casos en los que lo esté. Cuando el error está presente, usamos fmt.Println para mostrar nuestro error junto con un prefijo, como hemos hecho en ejemplos anteriores. Por último, usamos una instrucción return para omitir la ejecución de fmt.Println("Anchors away!"), dado que solo se debe ejecutar cuando no se produce ningún error.

Nota: La construcción if err ! = nil que se muestra en el último ejemplo es el caballo de batalla de la manipulación de errores en el lenguaje de programación Go. Donde sea que una función pueda producir un error, es importante utilizar una instrucción if para determinar su presencia. De esta manera, el código idiomático Go tiene, naturalmente, su lógica “happy path” en el primer nivel de indentación, y toda la lógica “sad path”, en el segundo.

Las instrucciones if tienen una cláusula opcional de asignación que puede utilizarse para ayudar a resumir la invocación de una función y el manejo de sus errores.

Ejecute el siguiente programa para ver la misma salida de nuestro ejemplo anterior, pero, esta vez, con una instrucción if compuesta para reducir el texto estándar:

package main

import (
    "errors"
    "fmt"
)

func boom() error {
    return errors.New("barnacles")
}

func main() {
    if err := boom(); err != nil {
        fmt.Println("An error occurred:", err)
        return
    }
    fmt.Println("Anchors away!")
}
Output
An error occurred: barnacles

Al igual que antes, tenemos una función, boom(), que siempre devuelve un error. Asignamos el error devuelto de boom() a err como la primera parte de la instrucción if. De esta manera, esa variable err está disponible en la segunda parte de la instrucción if, después del punto y coma. Comprobamos si el error estaba presente y mostramos nuestro error con una cadena de prefijo corta, como hemos hecho anteriormente.

En esta sección, aprendimos a manejar funciones que solo devuelven un error. Estas funciones son habituales, pero también es importante poder manejar errores de funciones que pueden devolver varios valores.

Devolución de errores junto con valores

Las funciones que devuelven un solo valor de error suelen ser las que producen algún cambio de estado, como la inserción de filas a una base de datos. También es frecuente escribir funciones que devuelven un valor si se completaron con éxito junto con un posible error si la función falló. Go permite que las funciones devuelvan más de un resultado, lo que puede utilizarse para devolver simultáneamente un valor y un tipo de error.

Para crear una función que devuelva más de un valor, enumeramos los tipos de cada valor devuelto dentro de paréntesis en la firma de la función. Por ejemplo, una función capitalize que devuelve una string y un error se declararía utilizando func capitalize(name string) (string, error) {}. La parte (string, error) le indica al compilador de Go que esta función devolverá una string y un error, en ese orden.

Ejecute el programa que se indica a continuación para ver la salida de una función que devuelve una string y un error:

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil
}

func main() {
    name, err := capitalize("sammy")
    if err != nil {
        fmt.Println("Could not capitalize:", err)
        return
    }

    fmt.Println("Capitalized name:", name)
}
Output
Capitalized name: SAMMY

Definimos capitalize() como una función que toma una cadena (el nombre que se escribirá en mayúsculas), y devuelve una cadena y un valor de error. En main(), invocamos capitalize() y asignamos los dos valores devueltos de la función a las variables name y err al separarlos con comas en el lado izquierdo del operador :=. A continuación, ejecutamos nuestra comprobación if err ! = nil, como en ejemplos anteriores, y mostramos el error en la salida estándar utilizando fmt.Println si el error estaba presente. Si no hubo errores, mostramos Capitalized name: SAMMY.

Intente cambiar la cadena "sammy" en name, err := capitalize("sammy") por la cadena vacía ("") y recibirá, en su lugar, el error Could not capitalize: no name provided.

La función capitalize devolverá un error cuando quienes invoquen la función proporcionen una cadena vacía para el parámetro name. Cuando el parámetro name no es la cadena vacía, capitalize() utiliza strings.ToTitle para escribir, en mayúsculas, el parámetro name y devuelve nil como valor de error.

Hay algunas convenciones sutiles que siguen este ejemplo que son típicas del código Go, pero el compilador de Go no las aplica. Cuando una función devuelve varios valores, con un error incluido, la convención requiere que el error se devuelva como último elemento. Al devolver un error de una función con varios valores de retorno, el código idiomático Go también establecerá un valor de cero para cada valor non-error. Los valores de cero son, por ejemplo, una cadena vacía para cadenas, 0 para enteros, una estructura vacía para tipos de estructura y nil para tipos de punteros e interfaces, por nombrar algunos. Abordaremos los valores de cero con más detalle en nuestro tutorial sobre variables y constantes.

Reducción del texto estándar

Respetar estas convenciones puede volverse tedioso en situaciones en las que se devuelven muchos valores de una función. Podemos utilizar una función anónima para ayudar a reducir el texto estándar. Las funciones anónimas son procedimientos que se asignan a variables. A diferencia de las funciones que definimos en ejemplos anteriores, solo están disponibles en las funciones donde se declaran. Esto las hace perfectas para actuar como fragmentos cortos de lógica auxiliar reutilizable.

El programa que se indica a continuación modifica el último ejemplo para incluir la longitud del nombre que escribiremos en mayúsculas. Dado que tiene tres valores que devolver, el manejo de errores podría tornarse engorroso sin una función anónima de ayuda:

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, int, error) {
    handle := func(err error) (string, int, error) {
        return "", 0, err
    }

    if name == "" {
        return handle(errors.New("no name provided"))
    }

    return strings.ToTitle(name), len(name), nil
}

func main() {
    name, size, err := capitalize("sammy")
    if err != nil {
        fmt.Println("An error occurred:", err)
    }

    fmt.Printf("Capitalized name: %s, length: %d", name, size)
}
Output
Capitalized name: SAMMY, length: 5

Ahora, dentro de main(), capturamos los tres argumentos devueltos de capitalize como name, size y err, respectivamente. Luego, comprobamos si capitalize devolvió un error al verificar si la variable err era distinta de nil. Es importante hacerlo antes de intentar utilizar cualquiera de los demás valores que devuelve capitalize, dado que la función anónima, handle, podría establecer esos valores en cero. Dado que no ocurrió ningún error porque proporcionamos la cadena "sammy", mostramos el nombre en mayúsculas y su longitud.

Una vez más, puede intentar cambiar "sammy" por la cadena vacía ("") para mostrar el caso de error (An error occurred: no name provided).

Dentro de capitalize, definimos la variable handle como una función anónima. Toma un solo error y devuelve valores idénticos en el mismo orden que los valores de retorno de capitalize. handle establece esos valores en cero y reenvía el error pasado como su argumento, como el valor de retorno final. Al usar esto, podemos devolver cualquier error que se detecte en capitalize al utilizar la instrucción return delante de la invocación a handle con el error como su parámetro.

Recuerde que capitalize siempre debe devolver tres valores, dado que así es como definimos la función. A veces, no queremos lidiar con todos los valores que una función podría devolver. Afortunadamente, tenemos cierta flexibilidad en la manera en que podemos utilizar estos valores en el lado de la asignación.

Manejo de errores de funciones que devuelven varios valores

Cuando una función devuelve muchos valores, Go requiere que asignemos una variable a cada uno de ellos. En el último ejemplo, lo hacemos al proporcionar nombres para los dos valores que devuelve la función capitalize. Estos nombres se deben separar con comas y tienen que aparecer en lado izquierdo del operador :=. El primer valor devuelto de capitalize se asignará a la variable name, y el segundo valor (el error) se asignará a la variable err. A veces, solo nos interesa el valor de error. Puede descartar cualquier valor no deseado que devuelva una función al utilizar el nombre de variable especial _.

En el programa que se indica a continuación, modificamos nuestro primer ejemplo con la función capitalize para producir un error al pasar la cadena vacía (""). Intente ejecutar este programa para ver cómo podemos examinar solo el error al descartar el primer valor devuelto con la variable _:

package main

import (
    "errors"
    "fmt"
    "strings"
)

func capitalize(name string) (string, error) {
    if name == "" {
        return "", errors.New("no name provided")
    }
    return strings.ToTitle(name), nil
}

func main() {
    _, err := capitalize("")
    if err != nil {
        fmt.Println("Could not capitalize:", err)
        return
    }
    fmt.Println("Success!")
}
Output
Could not capitalize: no name provided

Esta vez, dentro de la función main(), asignamos el nombre en mayúsculas (la string que se devuelve primero) a la variable de guion bajo (_). Al mismo tiempo, asignamos el error devuelto por capitalize a la variable err. A continuación, comprobamos si el error estaba presente en el condicional if err ! = nil. Dado que predefinimos una cadena vacía en el código como argumento de capitalize en la línea _, err := capitalize(""), este condicional siempre evaluará a true. Esto produce la salida "Could not capitalize: no name provided" que se muestra al invocar la función fmt.Println dentro del cuerpo de la instrucción if. Después de esto. el return omitirá fmt.Println("Success!").

Conclusión

Hemos visto muchas maneras de crear errores utilizando la biblioteca estándar y cómo crear funciones que devuelvan errores de una manera idiomática. En este tutorial, hemos logrado crear con éxito varios errores utilizando la biblioteca estándar errors.New y funciones fmt.Errorf. En tutoriales futuros, veremos cómo crear nuestros propios tipos de errores personalizados para transmitir más información a los usuarios.

0 Comments

Creative Commons License