tutoriales.com

Manejo Eficiente de Errores en Go: Estrategias y Buenas Prácticas para Código Robusto

En Go, el manejo de errores es una parte fundamental del desarrollo de software robusto. Este tutorial te guiará a través de las estrategias clave para gestionar errores de forma eficiente, desde la creación de tipos de error personalizados hasta el uso adecuado de `panic` y `recover`.

Intermedio10 min de lectura5 views15 de marzo de 2026Reportar error

El manejo de errores en Go es distintivo y, a menudo, una de las primeras cosas que los nuevos programadores notan. A diferencia de las excepciones en otros lenguajes, Go utiliza un enfoque explícito, devolviendo valores de error junto con el resultado de una operación. Este tutorial profundiza en las mejores prácticas y técnicas avanzadas para construir aplicaciones Go que no solo funcionen, sino que también manejen los fallos de manera elegante y predictiva.

🎯 Introducción al Manejo de Errores en Go

Go fomenta un manejo de errores explícito, lo que significa que las funciones que pueden fallar típicamente devuelven dos valores: el resultado y un error. Si el error es nil, la operación fue exitosa; de lo contrario, el valor de error describe el problema. Este paradigma obliga a los desarrolladores a considerar el manejo de errores en cada punto donde puede ocurrir un fallo, lo que conduce a un código más robusto y predecible.

package main

import (
	"errors"
	"fmt"
)

func divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("no se puede dividir por cero")
	}
	return a / b, nil
}

func main() {
	res, err := divide(10, 2)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("Resultado de la división:", res)

	res, err = divide(10, 0)
	if err != nil {
		fmt.Println("Error:", err)
		return
	}
	fmt.Println("Resultado de la división:", res)
}

Este ejemplo básico ilustra el patrón (result, error) que es omnipresente en Go. Siempre que una función pueda fallar, es su responsabilidad devolver un error.

💡 Consejo: Acostúmbrate a revisar `if err != nil` inmediatamente después de cada llamada a función que devuelva un error. Este es el `idiom` de Go.

🛠️ Tipos de Errores Personalizados y su Ventaja

Aunque errors.New es útil para errores simples, a menudo necesitas errores que contengan más contexto o que puedan ser diferenciados programáticamente. Go permite definir tus propios tipos que implementan la interfaz error.

La interfaz error es muy simple:

package main

type error interface {
	Error() string
}

📝 Definiendo un Tipo de Error Estructurado

Crear un tipo de error personalizado te permite incluir campos adicionales que proporcionan más detalles sobre el fallo. Esto es invaluable para la depuración y para tomar decisiones programáticas basadas en el tipo o la naturaleza del error.

package main

import "fmt"

type CustomError struct {
	Code    int
	Message string
	Op      string // Operación que causó el error
}

func (e *CustomError) Error() string {
	return fmt.Sprintf("Operación '%s' falló con código %d: %s", e.Op, e.Code, e.Message)
}

func performOperation(input int) error {
	if input < 0 {
		return &CustomError{Code: 1001, Message: "Entrada no puede ser negativa", Op: "validate_input"}
	}
	if input > 100 {
		return &CustomError{Code: 1002, Message: "Entrada excede el límite", Op: "process_data"}
	}
	return nil
}

func main() {
	err := performOperation(-5)
	if err != nil {
		fmt.Println(err)
		// Podemos realizar type assertion para acceder a los campos específicos
		if ce, ok := err.(*CustomError); ok {
			fmt.Printf("Detalles del error: Código=%d, Operación='%s'\n", ce.Code, ce.Op)
		}
	}

	err = performOperation(150)
	if err != nil {
		fmt.Println(err)
		if ce, ok := err.(*CustomError); ok {
			fmt.Printf("Detalles del error: Código=%d, Operación='%s'\n", ce.Code, ce.Op)
		}
	}

	err = performOperation(50)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Println("Operación exitosa.")
	}
}

Al usar aserciones de tipo (err.(*CustomError)), puedes inspeccionar los detalles del error y reaccionar de manera diferente según el Code o Op del error.

📦 Envoltorio de Errores (Error Wrapping) con fmt.Errorf y errors

Go 1.13 introdujo el envoltorio de errores, una característica crucial para añadir contexto a un error sin perder el error original. Esto es esencial para la depuración, ya que te permite ver la cadena completa de errores, desde la causa raíz hasta el punto donde se maneja el error.

La función fmt.Errorf con el verbo %w (wrap) y las funciones del paquete errors (Is, As, Unwrap) son las herramientas principales para esto.

🔗 fmt.Errorf y %w

%w permite envolver un error existente en uno nuevo, creando una cadena de errores.

package main

import (
	"errors"
	"fmt"
)

var ErrDatabaseConnection = errors.New("error de conexión a la base de datos")
var ErrInvalidQuery = errors.New("consulta SQL inválida")

func queryDatabase(sql string) error {
	if sql == "" {
		return ErrInvalidQuery
	}
	// Simula un fallo de conexión a la DB
	return fmt.Errorf("fallo al conectar: %w", ErrDatabaseConnection)
}

func getData(sql string) error {
	err := queryDatabase(sql)
	if err != nil {
		// Envuelve el error de la capa inferior con un contexto adicional
		return fmt.Errorf("no se pudo obtener datos: %w", err)
	}
	return nil
}

func main() {
	err := getData("SELECT * FROM users")
	if err != nil {
		fmt.Println("Error principal:", err)

		// Comprobar si el error es de un tipo específico en la cadena de errores
		if errors.Is(err, ErrDatabaseConnection) {
			fmt.Println("¡Es un error de conexión a la base de datos!")
		}

		if errors.Is(err, ErrInvalidQuery) {
			fmt.Println("¡Es una consulta inválida!")
		}

		// Desenvolver el error para acceder al error subyacente
		fmt.Println("Error subyacente (Unwrap):", errors.Unwrap(err))
	}

	fmt.Println("\n---")

	err = getData("") // Simula una consulta vacía
	if err != nil {
		fmt.Println("Error principal:", err)
		if errors.Is(err, ErrInvalidQuery) {
			fmt.Println("¡Es una consulta inválida!")
		}
	}
}

🧐 Funciones errors.Is, errors.As y errors.Unwrap

  • errors.Is(err, target): Comprueba si err o cualquiera de los errores que envuelve es target. Es útil para comparar errores con sentinelas predefinidas.
  • errors.As(err, target): Busca en la cadena de errores el primer error que puede ser asignado al tipo target y, si lo encuentra, asigna ese error a target. Es para errores estructurados.
  • errors.Unwrap(err): Devuelve el error subyacente envuelto por err, si lo hay.

Estas funciones son fundamentales para un manejo de errores robusto y diferenciado, permitiendo a tu código reaccionar a la causa raíz de un error, no solo a su manifestación más superficial.

⚠️ Advertencia: Evita envolver errores genéricos sin añadir contexto útil. Cada envoltorio debe proporcionar información adicional sobre *por qué* y *dónde* ocurrió el error.

😱 Pánico y Recuperación (panic y recover)

Go tiene panic y recover, que son similares a las excepciones en otros lenguajes, pero su uso es desaconsejado para el manejo de errores normales. panic debe reservarse para situaciones excepcionales e irrecuperables que indican un fallo de programación o un estado irrecuperable de la aplicación, como la imposibilidad de inicializar un componente crítico.

💣 Cuándo usar panic

  • Cuando tu programa no puede continuar de ninguna manera (ej. un servicio crítico no puede iniciar).
  • Fallos de programación (ej. acceso a índice fuera de rango que no debería ocurrir).

🩹 Cuándo usar recover

recover solo es útil dentro de una función defer. Captura el valor de panic si se está produciendo una panic en la misma goroutine. Esto permite que el programa se recupere del panic y continúe su ejecución, o para realizar limpieza antes de que el programa termine.

package main

import "fmt"

func mightPanic() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Se recuperó de un pánico:", r)
			// Opcional: registrar el pánico, enviar métricas, etc.
		}
	}()

	fmt.Println("Iniciando función que podría entrar en pánico...")

	// Simular una condición de pánico
	var s []int
	fmt.Println(s[0]) // Esto causará un pánico: index out of range

	fmt.Println("Esta línea no se ejecutará.")
}

func main() {
	fmt.Println("Programa principal iniciado.")
	mightPanic()
	fmt.Println("Programa principal continuó después de la recuperación.")

	fmt.Println("\n--- Un pánico sin recuperación ---")
	// Este pánico terminará el programa
	// func() {
	// 	panic("¡Pánico no recuperado!")
	// }()
	// fmt.Println("Esta línea nunca se verá si el pánico anterior no se recupera.")
}
🔥 Importante: La mayoría de las aplicaciones bien diseñadas rara vez usarán `panic` y `recover`. El manejo de errores explícito con `error` es casi siempre la opción preferida.

📊 Comparación de Estrategias de Errores

Característicaerror (Retorno de valores)panic / recover
FilosofíaExplícito, predictivo, para fallos esperados.Implícito, para fallos inesperados/irrecuperables.
Uso comúnValidación de entrada, errores de I/O, fallos de red/DB.Errores de programación, inicialización de componentes críticos.
Flujo de controlControlado, el llamador decide cómo manejarlo.Interrumpe el flujo normal, se propaga hacia arriba en la pila.
RendimientoLigero, sin sobrecarga significativa.Mayor sobrecarga debido a la gestión de la pila de llamadas.
VisibilidadEl contrato de la función lo indica ((T, error)).No evidente por la firma de la función.
RecuperabilidadFácilmente recuperable, parte del flujo normal.Requiere defer y recover, más complejo.
Buenas prácticasPreferido para la mayoría de los escenarios.Usar con moderación y solo para casos excepcionales.
Inicio ¿Es un fallo esperado y manejable? error NO ¿Es un fallo irrecuperable? panic NO Reconsiderar FIN

📈 Buenas Prácticas y Patrones Comunes

✅ Nombra tus errores centinela

Define errores var a nivel de paquete para errores comunes y comparables, como io.EOF.

package mypackage

import "errors"

var ErrNotFound = errors.New("elemento no encontrado")
var ErrPermissionDenied = errors.New("permiso denegado")

func FindUser(id int) (string, error) {
	// ... lógica de búsqueda ...
	if id == 100 {
		return "", ErrNotFound
	}
	return "User John Doe", nil
}

🪵 Registro de Errores (Error Logging)

Es crucial registrar los errores con suficiente contexto. Usa bibliotecas de logging (log estándar o logrus, zap) para añadir detalles relevantes.

package main

import (
	"errors"
	"fmt"
	"log"
)

func processRequest(id string) error {
	if id == "" {
		return errors.New("ID de solicitud vacío")
	}
	// Simular un error interno
	return fmt.Errorf("fallo al procesar ID '%s': %w", id, errors.New("error de servicio externo"))
}

func main() {
	log.SetFlags(log.LstdFlags | log.Lshortfile)

	err := processRequest("123")
	if err != nil {
		log.Printf("Error crítico en el manejador de solicitudes: %v", err)
		// Aquí podrías decidir si devuelves un error genérico al cliente
		// o si el error original contiene información sensible que no debe exponerse.
	}

	err = processRequest("")
	if err != nil {
		log.Printf("Error de validación: %v", err)
	}
}
📌 Nota: Al registrar errores, `log.Printf("%v", err)` es común, pero para errores envueltos, `log.Printf("%+v", err)` (con el paquete `github.com/pkg/errors` o en Go 1.13+ con `fmt.Errorf`) puede mostrar la cadena de errores y el stack trace, lo cual es invaluable para depuración.

🔄 Retorno temprano (fail fast)

El patrón de fail fast es común en Go: maneja los errores y retorna tan pronto como sea posible para evitar que el código continúe con un estado inválido.

func readConfig(path string) (*Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("no se pudo leer el archivo de configuración %s: %w", path, err)
	}

	var cfg Config
	err = json.Unmarshal(data, &cfg)
	if err != nil {
		return nil, fmt.Errorf("no se pudo parsear el JSON de configuración de %s: %w", path, err)
	}

	return &cfg, nil
}

📝 Opciones para el Manejo de Errores

Hay varias maneras de manejar un error devuelto:

  1. Propagarlo: Devolver el error al llamador (return err). Este es el más común.
  2. Manejarlo y recuperarse: Intentar una acción alternativa o corregir el estado.
  3. Registrarlo y continuar: Registrar el error pero permitir que el programa continúe si el error no es crítico.
  4. Terminar el programa: Si el error es fatal, log.Fatal o panic.

El manejo de errores en Go es una disciplina que requiere consideración en cada paso del desarrollo. Al aplicar estas estrategias, crearás aplicaciones más robustas, depurables y mantenibles.


¿Qué es un error sentinela? Un error sentinela es una variable de error pre-declarada (generalmente usando `errors.New`) que se compara directamente con un error devuelto. Por ejemplo, `io.EOF` es un error sentinela. Permite a los llamadores verificar si un error específico ocurrió usando `errors.Is`.
¿Cuándo debo usar `panic` en lugar de devolver un `error`? `panic` debe reservarse para situaciones excepcionales e irrecuperables, como fallos de programación (ej. errores de índice fuera de límites que no deberían haber ocurrido) o la imposibilidad de inicializar componentes críticos que impiden que la aplicación funcione. Para la mayoría de los escenarios de fallo esperados (errores de red, validación, DB), devuelve un `error`.
Tutorial Completado

Tutoriales relacionados

Comentarios (0)

Aún no hay comentarios. ¡Sé el primero!