5️⃣

Clase 5: Manejo de errores y excepciones


Objetivos
  1. Implementar un manejo adecuado de errores y excepciones en Go para garantizar la robustez y confiabilidad del programa.
  1. Utilizar las sentencias "defer" y "panic/recover" de manera efectiva en Go para mejorar la legibilidad y la gestión de errores en el código.
  1. Crear errores personalizados en Go para proporcionar información específica y detallada sobre situaciones excepcionales o fallos en el programa.

Contenido de la clase:

1. Tratamiento de errores y excepciones en Go

La gestión de errores o tratamiento de errores es una de las cosas con la que los programadores nos encontramos todos los días y hay que darle la importancia que merece.

Mientras escribes un programa, se ha de tener en cuenta las distintas formas en que se puede producir errores en los programas y debemos administrarlos correctamente. 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.

  • Go, un lenguaje sin exepciones

    A diferencia de otros lenguajes de programación como Java o Python, no existe un sistema de excepciones tradicional. En su lugar, Go utiliza el enfoque de manejo de errores basado en el tipo error y las sentencias panic y recover. para administrar un comportamiento inesperado en los programas.

    La razón principal detrás de esta decisión de diseño en Go fue simplificar y fomentar un código más limpio y legible. En lugar de utilizar bloques de try-catch para manejar excepciones, Go se enfoca en el manejo explícito de errores de tipo error.

    Los valores de tipo error pueden ser retornados por funciones y verificados utilizando la declaración condicional if. Esto permite un manejo claro y explícito de los errores en el código.

package main
import (
	"fmt"
	"strconv"
)

func main() {
	var age string
	fmt.Println("Ingresa tu edad: ")
	fmt.Scanln(&age)

	// Convertimos string a entero
	// la función strconv.Atoi devuelve dos valores, un entero y un error
	newAge, err := strconv.Atoi(age)

	// Si el error es nil no existe un error
	// nil == null
	if err != nil {
		fmt.Println("Ocurrió un error: ")
		fmt.Println(err)
	} else {
		fmt.Println("Tu edad en 10 años será: ", newAge+10)
	}
}
2. Uso de la sentencia “defer” y “panic/recover”
  • Defer

    Defer es una característica que permite que Go aplace la ejecución de una función. Este puede ser por ejemplo cuando otra función termina de ejecutarse.

    El aplazamiento de funciones es generalmente utilizado para realizar tareas de limpieza, una vez que se han completado las actividades a realizar por parte del algoritmo, como cerrar conexiones a bases de datos, borrar archivos temporales, limpiar el caché, liberar la memoria, etc.

    Supongamos ahora que deseamos conectarnos a una base de datos, y queremos que después de todas las operaciones se realice automáticamente la desconexión, independientemente del flujo de trabajo.

package main
import "fmt"

func conectar() {
	fmt.Println("Se ha conectado a la base de datos")
}

func desconectar() {
	fmt.Println("Se ha desconectado de la base de datos")
}

func leer() {
	fmt.Println("Se han leido los registros de la base de datos")
}

func actualizar() {
	fmt.Println("Se han actalizado registros de la base de datos")
}

func main() {
	conectar() // Se ejecuta 1°
	defer desconectar() // Se ejecuta 4° [último]
	leer() // Se ejecuta 2°
	actualizar() // Se ejecuta 3°
}
  • Panic

    En Go existe una función llamda panic que detiene el flujo de un programa e inicia un proceso de pánico. No es quizás una buena idea su uso, debido a que detener la ejecución del programa no ofrece ninguna salida posible.

    No se recomienda el uso de panic al menos que exista una serie de condiciones en las cuales el sistema no se pueda recuperar tales como:

    • Si el sistema continua su ejecución, más problemas serán generados, por lo que hay que detenerlo inmediatamente.
    • Existe un escenario que no ha sido cubierto y no se puede manejar, por ende hay que evitarlo.
package main
import "fmt"

func main() {
	var availableMoney float64 = 1000
	var cash float64

	for {
		fmt.Println("¿Cuánto dinero deseas retirar?")
		fmt.Scanln(&cash)

		if availableMoney < cash {
			// fmt.Println("No hay fondos disponibles")
			panic("Su tarjeta ha sido bloqueada")
		}
		availableMoney -= cash
		fmt.Printf("Se retiró %.2f, saldo disponible %.2f \n", cash, availableMoney)
	}
}
  • Recover

    En Go, la función recover se utiliza en combinación con la sentencia defer para capturar y manejar un panic ocurrido durante la ejecución de un programa.

    Cuando se produce un panic en una función, normalmente la ejecución se detiene. La función recover permite recuperarse del panic y continuar con la ejecución del programa en un estado controlado.

  1. Refactorizamos
package main
import "fmt"

func myAccount() {
	var availableMoney float64 = 1000
	var cash float64

	for {
		fmt.Println("¿Cuánto dinero deseas retirar?")
		fmt.Scanln(&cash)

		if availableMoney < cash {
			panic("blockCard")
		}
		availableMoney -= cash
		fmt.Printf("Se retiró %.2f, saldo disponible %.2f \n", cash, availableMoney)
	}
}

func main() {
	myAccount()
	fmt.Println("Finalizó la operación")
}
  1. Creamos la función handlePanic()
func blockCard() {
	r := recover()
	if r != nil {
		fmt.Println(r)
		fmt.Println("Bloquea la tarjeta")
		fmt.Println("Contacta al cliente por teléfono")

	}
}
  1. Utilizamos la función handlePanic() en la función myAccount()
func myAccount() {
	defer handlePanic()
	var availableMoney float64 = 1000
	var cash float64

	for {
		fmt.Println("¿Cuánto dinero deseas retirar?")
		fmt.Scanln(&cash)

		if availableMoney < cash {
			// fmt.Println("No hay fondos disponibles")
			panic("blockCard")
		}
		availableMoney -= cash
		fmt.Printf("Se retiró %.2f, saldo disponible %.2f \n", cash, availableMoney)
	}
}
  1. Código Final
package main
import "fmt"

func handlePanic() {
	r := recover()
	if r != nil {
		fmt.Println(r)
		fmt.Println("Bloquea la tarjeta")
		fmt.Println("Contacta al cliente por teléfono")

	}
}

func myAccount() {
	defer handlePanic()
	var availableMoney float64 = 1000
	var cash float64

	for {
		fmt.Println("¿Cuánto dinero deseas retirar?")
		fmt.Scanln(&cash)

		if availableMoney < cash {
			panic("blockCard")
		}
		availableMoney -= cash
		fmt.Printf("Se retiró %.2f, saldo disponible %.2f \n", cash, availableMoney)
	}
}

func main() {
	myAccount()
	fmt.Println("Finalizó la transacción")
}
💡
3. Creación de errores personalizados

Go ofrece dos métodos para crear errores en la biblioteca estándar: errors.New y fmt.Errorf.

La creación de errores personalizados puede ser útil en varias situaciones

  • Comunicar información específica: Al crear tus propios errores personalizados, puedes proporcionar mensajes de error más descriptivos y relevantes para tu aplicación. Esto ayuda a los desarrolladores y usuarios a comprender mejor la causa del error y cómo solucionarlo.
  • Tratamiento de errores específicos: Puedes tener un mayor control sobre cómo se manejan y se progagan en tu programa. Puedes utilizar distintos tipos de errores personalizados para representar situaciones diferentes y tomar decisiones específicas según el tipo de error.
  • Mejorar la legibilidad y mantenibilidad del codigo: Puedes darle nombres descriptivos y utilizamos en tu código de manera semántica, lo que facilita la compresión y el seguimiento del flujo del programa.
  1. Ejemplo básico

La función errors.New es una función proporcionada por el paquete errors en Go. Su propósito es crear un nuevo error con un mensaje de texto específico.

Esta función recibe como argumento un string que representa el mensaje de error que se desea asociar al nuevo error creado. Devuelve un valor de tipo error, que es una interfaz en Go utilizada para representar errores.

package main
import (
	"errors"
	"fmt"
)

func main() {
	// err := fmt.Errorf("Error generado con fmt")
	err := errors.New("Error generado durante la ejecución")
	if err != nil {
		fmt.Println(err) // Error generado durante la ejecución
		fmt.Printf("%T", err) // *errors.errorString
	}
}
  1. Ejemplo Intermedio

Utilizamos errors.New en una función que retorna dos valores de tipo int, error

Creamos una función que retorna la división de dos números enteros. Sí el divisor es 0, la función debe retornar un error.

package main

import (
	"errors"
	"fmt"
)

func divide(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("División por cero no es posible")
	}
	return a / b, nil
}

func main() {
	result, err := divide(10, 0)
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("Resultado:", result)
	}
}
  1. Ejemplo Avanzado

La diferencia entre usar errors.New y crear tu porpio tipo de error con un método Error() radica en la flexibilidad y personalización que deseas tener al crear y manejar errores en Go.

package main
import (
	"fmt"
)

/* 
Interface implicito - No es necesario declarar esta interface
type error interface {
    Error() string
}
*/

type MyError struct {
	message string
	code    int
}

func (e MyError) Error() string {
	return fmt.Sprintf("Message: %s.\nStatusCode: %d", e.message, e.code)
}

func divide(a, b int) (int, error) {
	if b == 0 {
		return 0, MyError{message: "División por cero no es posible", code: 400}
	}
	return a / b, nil
}

func main() {
	result, err := divide(10, 0)
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("Resultado:", result)
	}
}
4. Tarea
  1. Escribe una función llamada SearchName que reciba un slice de nombres y un nombre objetivo. La función debe buscar el nombre objetivo en el slice y devolver su índice si se encuentra. Si el nombre no se encuentra, la función debe devolver un error personalizado del tipo NotFound.
// Solución
package main
import "fmt"

type NotFound struct {
	message string
}

func (e *NotFound) Error() string {
	return fmt.Sprintf("Error: el nombre '%s' no se encontró en la lista", e.message)
}

func SearchName(list []string, name string) (int, error) {
	for i, nombre := range list {
		if nombre == name {
			return i, nil
		}
	}
	return 0, &NotFound{message: name}
}

func main() {
	list := []string{"Juan", "María", "Pedro", "Ana"}
	name := "Pedro"

	index, err := SearchName(list, name)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Printf("El nombre '%s' se encontró en el índice %d\n", name, index)
	}
}
  1. Escribe una función llamada validatePositive que reciba un número entero y verifique si es un número positivo. Si el número es positivo, la función debe retornar true. Si el número es negativo o cero, la función debe retornar un error indicando que el número no es válido.
// Solución
package main
import (
	"errors"
	"fmt"
)

func validatePositive(num int) (bool, error) {
	if num <= 0 {
		return false, errors.New("El número no es válido")
	}
	return true, nil
}

func main() {
	number := 42

	valid, err := validatePositive(number)
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("El número es válido:", valid)
	}
}
5. Recursos