Más información sobre cómo controlar los errores en Go

Completado

Mientras escribe los programas, ha de tener en cuenta las distintas formas en que se puede producir errores en los programas y debe administrarlos. Los usuarios no necesitan ver un error de seguimiento de la pila largo y confuso. Es mejor si ven información significativa sobre lo que salió mal. Como ha visto, Go tiene funciones integradas como panic y recover para administrar excepciones, o un comportamiento inesperado, en sus programas. Pero los errores son problemas conocidos para cuyo control deben compilarse los programas.

El enfoque de Go en cuanto al control de errores es simplemente un mecanismo de flujo de control donde solo se necesitan una instrucción if y una instrucción return. Por ejemplo, cuando se llama a una función para obtener información de un objeto employee, es posible que quiera saber si existe el empleado. La forma recomendada de Go para controlar este error esperado sería similar a la siguiente:

employee, err := getInformation(1000)
if err != nil {
    // Something is wrong. Do something.
}

Observe que la función getInformation devuelve la estructura employee y además un error como segundo valor. El error podría ser nil. Si el error es nil, significa que se ha realizado correctamente. Si no es nil, significa que se produjo un error. Un error que no sea nil incluye un mensaje de error que se puede imprimir o, preferiblemente, registrar. Así es cómo se controlan los errores en Go. Abordaremos algunas otras estrategias en la sección siguiente.

Probablemente observe que el control de errores de Go exige que se preste más atención a cómo se notifica y controla un error. Precisamente de eso se trata. Echemos un vistazo a otros ejemplos para ayudarle a entender mejor el enfoque de Go en cuanto al control de errores.

Usaremos el fragmento de código que empleamos en el caso de las estructuras para practicar diversas estrategias de control de errores:

package main

import (
    "fmt"
    "os"
)

type Employee struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}

func main() {
    employee, err := getInformation(1001)
    if err != nil {
        // Something is wrong. Do something.
    } else {
        fmt.Print(employee)
    }
}

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    return employee, err
}

func apiCallEmployee(id int) (*Employee, error) {
    employee := Employee{LastName: "Doe", FirstName: "John"}
    return &employee, nil
}

A partir de aquí, nos centraremos en la modificación de las funciones getInformation, apiCallEmployeey main para mostrar cómo controlar los errores.

Estrategias de control de errores

Cuando una función devuelve un error, normalmente será el último valor devuelto. Como se vio en la sección anterior, es responsabilidad del autor de llamada comprobar si existe un error y administrarlo. Así que una estrategia habitual es seguir usando ese patrón para propagar el error en una subrutina. Por ejemplo, una subrutina (como getInformation en el ejemplo anterior) podría devolver el error al autor de llamada sin hacer nada más de la siguiente manera:

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    if err != nil {
        return nil, err // Simply return the error to the caller.
    }
    return employee, nil
}

También se podría querer incluir más información antes de propagar el error. Para ello, puede usarse la función fmt.Errorf(), que es similar a la que se ha visto antes, pero devuelve un error. Por ejemplo, podría agregarse más contexto al error y seguir devolviendo el error original de la siguiente manera:

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    if err != nil {
        return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)
    }
    return employee, nil
}

Otra estrategia consiste en ejecutar la lógica de reintento cuando los errores son transitorios. Por ejemplo, puede usar una directiva de reintentos para llamar a una función tres veces y esperar dos segundos de la siguiente manera:

func getInformation(id int) (*Employee, error) {
    for tries := 0; tries < 3; tries++ {
        employee, err := apiCallEmployee(1000)
        if err == nil {
            return employee, nil
        }

        fmt.Println("Server is not responding, retrying ...")
        time.Sleep(time.Second * 2)
    }

    return nil, fmt.Errorf("server has failed to respond to get the employee information")
}

Por último, en lugar de imprimir errores en la consola, podría registrar los errores y ocultar los detalles de la implementación a los usuarios finales. Trataremos el registro en el siguiente módulo. Por ahora, echemos un vistazo a cómo puede crear y usar errores personalizados.

Creación de errores reutilizables

A veces, el número de mensajes de error aumenta y se quiere mantener el orden. O tal vez prefiera crear una biblioteca para los mensajes de error comunes que quiera reutilizar. En Go, puede usar la función errors.New() para crear errores y volver a usarlos en diversos elementos de la siguiente manera:

var ErrNotFound = errors.New("Employee not found!")

func getInformation(id int) (*Employee, error) {
    if id != 1001 {
        return nil, ErrNotFound
    }

    employee := Employee{LastName: "Doe", FirstName: "John"}
    return &employee, nil
}

El código de la función getInformation es mejor y, si hay que cambiar el mensaje de error, se hace en un solo lugar. Además, observe que la convención consiste en incluir el prefijo Err para las variables de error.

Por último, si tiene una variable de error, podría ser más específico cuando controle un error en una función autor de llamada. La función errors.Is() permite comparar el tipo de error que se obtiene de la siguiente manera:

employee, err := getInformation(1000)
if errors.Is(err, ErrNotFound) {
    fmt.Printf("NOT FOUND: %v\n", err)
} else {
    fmt.Print(employee)
}

Aquí se indican algunas prácticas recomendadas que deben tenerse en cuenta al controlar errores en Go:

  • Compruebe siempre si hay errores, aunque no los espere. Luego, hay que controlarlos correctamente para evitar la exposición de información innecesaria a los usuarios finales.
  • Incluya un prefijo en un mensaje de error para conocer el origen del error. Por ejemplo, podría incluir el nombre del paquete y la función.
  • Cree tantas variables de error reutilizables como pueda.
  • Comprenda la diferencia entre utilizar la devolución de errores y la emisión de alertas de pánico. Emita alertas de pánico cuando no haya nada más que pueda hacer. Por ejemplo, si una dependencia no está lista, no tiene sentido que el programa funcione (a menos que quiera ejecutar un comportamiento predeterminado).
  • Registre los errores con tantos detalles como sea posible (veremos cómo en la sección siguiente) e imprima los errores que un usuario final puede entender.