A logo showing the text blog.marcnuri.com
English
Inicio»Desarrollo Backend»Buenas Prácticas para el Manejo de Errores en Go

Entradas Recientes

  • Fabric8 Kubernetes Client 7.4.0 está disponible!
  • Kubernetes MCP Server se une a la organización Containers
  • MCP Tool Annotations: Añadiendo Metadatos y Contexto a Tus Herramientas de IA
  • Fabric8 Kubernetes Client 7.2.0 está disponible!
  • Conectarse a un servidor MCP con JavaScript y AI SDK

Categorías

  • Antiguo
  • Cloud Native
  • Desarrollo Backend
  • Desarrollo Frontend
  • Herramientas
  • Ingeniería de calidad
  • Inteligencia Artificial
  • JavaScript
  • Operaciones
  • Personal
  • Proyectos personales
  • Reflexiones sobre Ingeniería

Archivos

  • septiembre 2025
  • julio 2025
  • mayo 2025
  • abril 2025
  • marzo 2025
  • febrero 2025
  • enero 2025
  • diciembre 2024
  • noviembre 2024
  • agosto 2024
  • junio 2024
  • mayo 2024
  • abril 2024
  • marzo 2024
  • febrero 2024
  • enero 2024
  • diciembre 2023
  • noviembre 2023
  • octubre 2023
  • septiembre 2023
  • agosto 2023
  • julio 2023
  • junio 2023
  • mayo 2023
  • abril 2023
  • marzo 2023
  • febrero 2023
  • enero 2023
  • diciembre 2022
  • noviembre 2022
  • octubre 2022
  • septiembre 2022
  • agosto 2022
  • julio 2022
  • junio 2022
  • mayo 2022
  • marzo 2022
  • febrero 2022
  • enero 2022
  • diciembre 2021
  • noviembre 2021
  • octubre 2021
  • septiembre 2021
  • agosto 2021
  • julio 2021
  • enero 2021
  • diciembre 2020
  • octubre 2020
  • septiembre 2020
  • agosto 2020
  • junio 2020
  • mayo 2020
  • marzo 2020
  • febrero 2020
  • enero 2020
  • noviembre 2019
  • septiembre 2019
  • julio 2019
  • diciembre 2018
  • agosto 2018
  • julio 2018
  • junio 2018
  • mayo 2018
  • marzo 2018
  • febrero 2018
  • noviembre 2017
  • octubre 2017
  • agosto 2017
  • julio 2017
  • enero 2017
  • julio 2016
  • enero 2016
  • diciembre 2015
  • noviembre 2015
  • diciembre 2014
  • marzo 2014
  • febrero 2011
  • junio 2008
  • mayo 2008
  • abril 2008
  • enero 2008
  • junio 2007
  • mayo 2007
  • abril 2007
  • marzo 2007

Buenas Prácticas para el Manejo de Errores en Go

2020-03-07 en Desarrollo Backend etiquetado Go / Buenas Prácticas / Depuración/Debugging por Marc Nuri | Última actualización: 2025-09-21
English version

Introducción

El manejo de errores es uno de los aspectos más distintivos de la programación en Go. A diferencia de lenguajes que usan excepciones como Java, Go trata los errores como valores que deben ser manejados explícitamente. Al principio, este enfoque puede parecer repetitivo, pero en la práctica lleva a código que es más fácil de razonar y depurar.

En este artículo, iré más allá de lo básico y compartiré la filosofía de manejo de errores de Go, patrones comunes, y trampas en las que he visto caer a desarrolladores (incluido yo mismo). Veremos prácticas idiomáticas, tipos de errores personalizados, wrapping, estrategias de depuración, y cómo aplicarlas en aplicaciones reales (web, base de datos, concurrencia). Al final, deberías ser capaz de diseñar flujos de errores que sean tanto confiables para los usuarios como mantenibles para los desarrolladores.

Filosofía del Manejo de Errores en Go

El enfoque de Go para el manejo de errores se basa en varios principios clave:

  1. Los errores son valores: se comportan como valores de retorno normales.
  2. Manejo explícito: debes verificarlos y actuar sobre ellos.
  3. Sin flujo de control oculto: no hay bloques try/catch que alteren la ejecución de forma inesperada.
  4. Fallar rápido: exponer problemas temprano en lugar de dejar que se propaguen silenciosamente.

Este estilo explícito obliga a los desarrolladores a pensar en los casos de error justo donde ocurren.

La interfaz error

En Go, un error es cualquier tipo que implemente la interfaz error:

error_interface.go
type error interface {
    Error() string
}

Un error básico puede crearse con errors.New():

basic_errors.go
package main

import (
    "errors"
    "fmt"
)

func divide(n, d float64) (float64, error) {
    if d == 0 {
        return 0, errors.New("division por cero")
    }
    return n / d, nil
}

func main() {
    result, err := divide(10, 0)
    if err != nil {
        fmt.Printf("Error: %v\n", err)
        return
    }
    fmt.Printf("Resultado: %.2f\n", result)
}

En este ejemplo, divide retorna un error si el denominador es cero.

En la función main, seguimos los principios clave:

  • Verificamos el error inmediatamente después de la llamada a la función.
  • Lo manejamos explícitamente imprimiendo un mensaje y retornando temprano.
  • Si no hay error, procedemos a usar el resultado.

Patrones Básicos de Manejo de Errores

El patrón estándar

La forma más común de manejar errores es:

standard_pattern.go
result, err := aFunction()
if err != nil {
    // Manejar el error y retornar temprano
    return err
}
// Continuar con el resultado

Antipatrón: ignorar el error:

// ❌ No hagas esto
result, _ := aFunction()

Esto, a menudo, oculta problemas reales hasta producción.

Múltiples valores de retorno

Las funciones de Go, frecuentemente retornan tanto un valor como un error:

multiple_returns.go
package main

import (
    "fmt"
    "strconv"
)

func parseAge(s string) (int, error) {
    age, err := strconv.Atoi(s)
    if err != nil {
        return 0, fmt.Errorf("formato de edad inválido: %w", err)
    }

    if age < 0 || age > 150 {
        return 0, fmt.Errorf("la edad %d está fuera del rango válido (0-150)", age)
    }

    return age, nil
}

func main() {
    validCases := []string{"25", "0", "150"}
    invalidCases := []string{"abc", "-5", "200", "25.5"}

    fmt.Println("Casos válidos:")
    for _, ageStr := range validCases {
        age, err := parseAge(ageStr)
        if err != nil {
            fmt.Printf("❌ %s: %v\n", ageStr, err)
        } else {
            fmt.Printf("✅ %s: %d\n", ageStr, age)
        }
    }

    fmt.Println("\nCasos inválidos:")
    for _, ageStr := range invalidCases {
        age, err := parseAge(ageStr)
        if err != nil {
            fmt.Printf("❌ %s: %v\n", ageStr, err)
        } else {
            fmt.Printf("✅ %s: %d\n", ageStr, age)
        }
    }
}

En este ejemplo, parseAge retorna tanto la edad parseada como un error si la entrada es inválida. La función main demuestra el manejo de entradas tanto válidas como inválidas, imprimiendo mensajes apropiados para cada caso.

Retornos tempranos

Evita bloques if profundamente anidados retornando temprano:

early_returns.go
func processUser(userID string) error {
    user, err := getUserByID(userID)
    if err != nil {
        return fmt.Errorf("error al obtener usuario: %w", err)
    }

    if !user.IsActive {
        return fmt.Errorf("el usuario %s no está activo", userID)
    }

    err = validateUser(user)
    if err != nil {
        return fmt.Errorf("validación de usuario falló: %w", err)
    }

    err = saveUser(user)
    if err != nil {
        return fmt.Errorf("error al guardar usuario: %w", err)
    }

    return nil
}

En este ejemplo, cada error es manejado inmediatamente, manteniendo el código plano y legible.

Tipos de Errores Personalizados

Los errores personalizados añaden contexto y pueden ser identificados de forma más precisa. Son útiles cuando tu dominio tiene distintas clases de error (validación, red, base de datos).

custom_errors.go
package main

import (
    "fmt"
    "net/http"
    "strings"

)

// ValidationError representa un error de validación
type ValidationError struct {
    Field   string
    Message string
}

func (e *ValidationError) Error() string {
    return fmt.Sprintf("error de validación en el campo '%s': %s", e.Field, e.Message)
}

// HttpError representa un error relacionado con http
type HttpError struct {
    StatusCode int
    URL        string
    Err        error
}

func (e *HttpError) Error() string {
    return fmt.Sprintf("error http (%d) para la URL %s: %v", e.StatusCode, e.URL, e.Err)
}

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

// DatabaseError representa un error de base de datos
type DatabaseError struct {
    Operation string
    Table     string
    Err       error
}

func (e *DatabaseError) Error() string {
    return fmt.Sprintf("error de base de datos durante %s en la tabla %s: %v", e.Operation, e.Table, e.Err)
}

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

// Funciones de ejemplo que retornan errores personalizados
func validateEmail(email string) error {
    if email == "" {
        return &ValidationError{
            Field:   "email",
            Message: "el email es obligatorio",
        }
    }

    if !strings.Contains(email, "@") {
        return &ValidationError{
            Field:   "email",
            Message: "el email debe contener el símbolo @",
        }
    }

    return nil
}

func fetchUserData(url string) error {
    // Simular petición http
    if url == "" {
        return &HttpError{
            StatusCode: http.StatusBadRequest,
            URL:        url,
            Err:        fmt.Errorf("URL vacía proporcionada"),
        }
    }

    // Simular error http
    return &HttpError{
        StatusCode: http.StatusNotFound,
        URL:        url,
        Err:        fmt.Errorf("usuario no encontrado"),
    }
}

func saveUserToDB(userID string) error {
    if userID == "" {
        return &DatabaseError{
            Operation: "INSERT",
            Table:     "users",
            Err:       fmt.Errorf("el ID de usuario no puede estar vacío"),
        }
    }

    return nil
}

func main() {
    // Probar error de validación
    err := validateEmail("email-invalido")
    if err != nil {
        fmt.Printf("Error de validación: %v\n", err)
    }

    // Probar error de red
    err = fetchUserData("")
    if err != nil {
        fmt.Printf("Error de red: %v\n", err)
    }

    // Probar error de base de datos
    err = saveUserToDB("")
    if err != nil {
        fmt.Printf("Error de base de datos: %v\n", err)
    }
}

En este ejemplo, definimos tres tipos de errores personalizados: ValidationError, HttpError, y DatabaseError. Cada tipo incluye campos relevantes e implementa el método Error() para proporcionar un mensaje descriptivo.

La función main llama a funciones que retornan estos errores personalizados e imprime los mensajes de error usando el verbo %v en fmt.Printf, que invoca el método Error() de cada tipo de error.

Consejo

No crees tipos de errores personalizados para cada caso. Úsalos solo donde el código downstream realmente necesite distinguir una falla de otra.

Wrapping y Unwrapping de Errores

Go 1.13 introdujo el wrapping, permitiéndote añadir contexto sin perder la causa original.

Wrapping básico de errores

error_wrapping.go
package main

import (
    "errors"
    "fmt"
    "os"
)

func readConfig(filename string) error {
    _, err := os.Open(filename)
    if err != nil {
        return fmt.Errorf("error al leer archivo de configuración %s: %w", filename, err)
    }
    return nil
}

func initialize() error {
    err := readConfig("blog.marcnuri.com.config.yaml")
    if err != nil {
        return fmt.Errorf("inicialización falló: %w", err)
    }
    return nil
}

func main() {
    err := initialize()
    if err != nil {
        fmt.Printf("Error de aplicación: %v\n", err)

        // Verificar tipos de errores específicos
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("El archivo de configuración no existe")
        }

        // Unwrap para obtener el error original
        var pathErr *os.PathError
        if errors.As(err, &pathErr) {
            fmt.Printf("Ocurrió un error de ruta: %s\n", pathErr.Path)
        }
    }
}

En este ejemplo, readConfig envuelve el error retornado por os.Open con contexto adicional. La función initialize envuelve este error adicionalmente. En main, imprimimos la cadena de errores completa y usamos errors.Is y errors.As para verificar tipos de errores específicos.

Veamos cómo usar estas funciones efectivamente.

Las funciones errors.Is y errors.As

Estas funciones te ayudan a detectar o extraer errores wrappeados:

errors_is_as.go
package main

import (
    "errors"
    "fmt"
    "os"
)

var (
    ErrUserNotFound = errors.New("usuario no encontrado")
    ErrInvalidInput = errors.New("entrada inválida")
)

func findUser(id string) error {
    if id == "" {
        return fmt.Errorf("ID de usuario vacío: %w", ErrInvalidInput)
    }

    if id == "404" {
        return fmt.Errorf("búsqueda de usuario falló: %w", ErrUserNotFound)
    }

    return nil
}

func main() {
    testCases := []string{"", "404", "123"}

    for _, userID := range testCases {
        err := findUser(userID)
        if err != nil {
            fmt.Printf("ID de Usuario %s: %v\n", userID, err)

            // Verificar errores específicos usando errors.Is
            if errors.Is(err, ErrUserNotFound) {
                fmt.Println("  → Este es un error de usuario no encontrado")
            }

            if errors.Is(err, ErrInvalidInput) {
                fmt.Println("  → Este es un error de entrada inválida")
            }

            // Verificar errores del sistema
            var pathErr *os.PathError
            if errors.As(err, &pathErr) {
                fmt.Printf("  → Error de ruta: %s\n", pathErr.Path)
            }
        } else {
            fmt.Printf("ID de Usuario %s: Éxito\n", userID)
        }
        fmt.Println()
    }
}

En este ejemplo, definimos dos sentinel errors: ErrUserNotFound y ErrInvalidInput. Estos errores son envueltos con contexto en la función findUser dependiendo de la entrada.

En la función main, probamos varios IDs de usuario e imprimimos los errores resultantes. Usamos errors.Is para verificar si el error coincide con nuestros sentinel errors y errors.As para verificar errores del sistema como os.PathError.

Consejo

Evita envolver en cada capa individual. Envuelve solo cuando estés añadiendo contexto útil; de lo contrario terminarás con cadenas de errores ruidosas e ilegibles.

Manejo de Errores en Diferentes Contextos

Ahora que hemos cubierto los fundamentos, veamos cómo aplicar estos patrones en escenarios del mundo real.

Aplicaciones web

Al construir APIs, un patrón común es convertir errores de Go en respuestas JSON estructuradas que los clientes puedan entender.

Aquí hay un ejemplo conciso que muestra cómo definir un tipo de error personalizado y una función helper para mapear errores a códigos de estado HTTP:

web_errors.go
package main

import (
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
)

type APIError struct {
    Code    string `json:"code"`
    Message string `json:"message"`
}

func (e *APIError) Error() string { return e.Message }

func writeError(w http.ResponseWriter, err error) {
    var apiErr *APIError
    if !errors.As(err, &apiErr) {
        apiErr = &APIError{"INTERNAL_ERROR", "error interno del servidor"}
    }

    status := map[string]int{
        "USER_NOT_FOUND": http.StatusNotFound,
        "INVALID_INPUT":  http.StatusBadRequest,
    }[apiErr.Code]
    if status == 0 {
        status = http.StatusInternalServerError
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(apiErr)
}

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    if id == "" {
        writeError(w, &APIError{"INVALID_INPUT", "el ID de usuario es obligatorio"})
        return
    }
    if id == "404" {
        writeError(w, &APIError{"USER_NOT_FOUND", fmt.Sprintf("no existe usuario con id %s", id)})
        return
    }
    json.NewEncoder(w).Encode(map[string]string{"id": id, "name": "Juan Pérez"})
}

func main() {
    http.HandleFunc("/user", getUserHandler)
    http.ListenAndServe(":8080", nil)
}

En este ejemplo, usamos un único tipo APIError para representar errores de aplicación y un helper writeError para traducirlos a respuestas JSON con el código de estado HTTP correcto. El helper usa errors.As para verificar si el error entrante ya es un APIError; de lo contrario, por defecto usa un error interno genérico.

Este patrón mantiene los handlers cortos y enfocados, mientras asegura que los clientes reciban consistentemente respuestas claras y estructuradas.

Operaciones de base de datos

Las operaciones de base de datos a menudo retornan errores específicos del driver como sql.ErrNoRows que necesitan manejo cuidadoso. Aquí hay un ejemplo simplificado que muestra cómo envolver, propagar e inspeccionar errores usando errors.Is de Go y sentinel errors:

database_errors.go
package main

import (
    "database/sql"
    "errors"
    "fmt"
    "log"
)

// User representa un usuario en el sistema
type User struct {
    ID    int
    Name  string
    Email string
}

// Errores sentinela
var (
    ErrUserNotFound  = errors.New("usuario no encontrado")
    ErrDuplicateUser = errors.New("usuario duplicado")
)

// UserRepository maneja operaciones de base de datos de usuarios
type UserRepository struct {
    db *sql.DB
}

func (r *UserRepository) GetUser(id int) (*User, error) {
    user := &User{}
    err := r.db.QueryRow("SELECT id, name, email FROM users WHERE id = ?", id).
        Scan(&user.ID, &user.Name, &user.Email)
    if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
            return nil, fmt.Errorf("usuario %d no encontrado: %w", id, ErrUserNotFound)
        }
        return nil, fmt.Errorf("error al consultar usuario: %w", err)
    }
    return user, nil
}

func (r *UserRepository) CreateUser(user *User) error {
    _, err := r.db.Exec("INSERT INTO users (name, email) VALUES (?, ?)", user.Name, user.Email)
    if err != nil {
        if isDuplicateKeyError(err) {
            return fmt.Errorf("ya existe usuario con email %s: %w", user.Email, ErrDuplicateUser)
        }
        return fmt.Errorf("error al crear usuario: %w", err)
    }
    return nil
}

// Función stub para verificación de duplicados
func isDuplicateKeyError(err error) bool { return false }

func main() {
    var db *sql.DB // imagina que esto está inicializado
    repo := &UserRepository{db: db}

    user, err := repo.GetUser(1)
    if err != nil {
        if errors.Is(err, ErrUserNotFound) {
            log.Printf("Usuario no encontrado: %v", err)
        } else {
            log.Printf("Error de base de datos: %v", err)
        }
        return
    }

    fmt.Printf("Usuario encontrado: %+v\n", user)
}

Este ejemplo demuestra cómo manejar errores de base de datos de manera concisa. La función GetUser retorna errores envueltos con contexto y usa errors.Is para verificar el error sentinel sql.ErrNoRows. La función CreateUser, de manera similar, envuelve errores y muestra cómo podrías manejar restricciones específicas de base de datos, como claves duplicadas.

La función main ilustra cómo llamar los métodos del repositorio y manejar errores apropiadamente verificando los sentinel errors específicos del dominio que definimos.

Al mantener el código plano y enfocado, puedes ver prácticas idiomáticas de Go para propagar e inspeccionar errores sin boilerplate innecesario.

Goroutines y Manejo de Errores

El código concurrente necesita recolección explícita de errores. Un patrón común es enviar resultados (incluyendo errores) a través de canales:

goroutine_errors.go
package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

// Result representa el resultado de una operación
type Result struct {
    Value int
    Error error
}

// worker realiza trabajo y envía resultados a través de un canal
func worker(ctx context.Context, id int, jobs <-chan int, results chan<- Result) {
    for {
        select {
        case job, ok := <-jobs:
            if !ok {
                return // Canal de trabajos cerrado
            }

            // Simular trabajo que puede fallar
            if job%3 == 0 {
                results <- Result{
                    Value: 0,
                    Error: fmt.Errorf("worker %d: trabajo %d falló (divisible por 3)", id, job),
                }
            } else {
                // Simular algo de tiempo de procesamiento
                time.Sleep(100 * time.Millisecond)
                results <- Result{
                    Value: job * job,
                    Error: nil,
                }
            }

        case <-ctx.Done():
            fmt.Printf("Worker %d cancelado: %v\n", id, ctx.Err())
            return
        }
    }
}

// main demuestra manejo de errores con goroutines
func main() {
    const numWorkers = 3
    const numJobs = 10

    jobs := make(chan int, numJobs)
    results := make(chan Result, numJobs)

    // Crear contexto con timeout
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    // Iniciar workers
    var wg sync.WaitGroup
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go func(workerID int) {
            defer wg.Done()
            worker(ctx, workerID, jobs, results)
        }(w)
    }

    // Enviar trabajos
    go func() {
        defer close(jobs)
        for j := 1; j <= numJobs; j++ {
            select {
            case jobs <- j:
            case <-ctx.Done():
                fmt.Println("Envío de trabajos cancelado")
                return
            }
        }
    }()

    // Recolectar resultados
    go func() {
        wg.Wait()
        close(results)
    }()

    // Procesar resultados y manejar errores
    var successCount, errorCount int
    for result := range results {
        if result.Error != nil {
            fmt.Printf("❌ Error: %v\n", result.Error)
            errorCount++
        } else {
            fmt.Printf("✅ Resultado: %d\n", result.Value)
            successCount++
        }
    }

    fmt.Printf("\nResumen: %d exitosos, %d fallidos\n", successCount, errorCount)
}

En este ejemplo, definimos una estructura Result para encapsular tanto el valor como cualquier error de un worker. La función worker procesa trabajos y envía resultados a través de un channel. La función main configura los worker channels, inicia múltiples workers, y recolecta resultados mientras maneja errores apropiadamente.

Para una introducción más amplia a la concurrencia, echa un vistazo a Patrones de Concurrencia en Go: Goroutines y Channels.

Buenas Prácticas para el Manejo de Errores

Ahora que hemos cubierto los fundamentos y visto ejemplos prácticos, resumamos algunas de las mejores prácticas para el manejo de errores en Go.

1. Siempre verificar errores

Nunca ignores errores a menos que tengas una razón específica:

check_errors.go
// ❌ Malo
result, _ := aFunction()

// ✅ Bueno
result, err := aFunction()
if err != nil {
    return fmt.Errorf("operación falló: %w", err)
}

2. Proporcionar contexto en mensajes de error

Los mensajes de error deben ser descriptivos y proporcionar contexto sobre qué operación falló. También deben proporcionar información accionable para que el caller pueda entender y potencialmente corregir el problema.

contextual_errors.go
// ❌ Malo
return errors.New("falló")

// ✅ Bueno
return fmt.Errorf("error al procesar usuario %s de blog.marcnuri.com: formato de email inválido", userID)

Nota

Los mensajes de error deben estar en minúsculas ya que a menudo son wrapeados en oraciones más grandes.

3. Usar wrapping de errores para cadenas de llamadas

Preservar el error original mientras añades contexto:

wrapping_errors.go
func processUser(userID string) error {
    user, err := getUser(userID)
    if err != nil {
        return fmt.Errorf("error al procesar usuario %s de blog.marcnuri.com: %w", userID, err)
    }
    // ... resto de la función
    return nil
}

Nota el uso de %w en fmt.Errorf para envolver el error original.

4. Crear tipos de errores personalizados para errores específicos del dominio

Usa tipos de errores de dominio personalizados cuando necesites transmitir condiciones de error específicas que los callers podrían querer verificar:

custom_error_type.go
type UserNotFoundError struct {
    UserID string
}

func (e *UserNotFoundError) Error() string {
    return fmt.Sprintf("usuario %s de blog.marcnuri.com no encontrado", e.UserID)
}

5. No usar panic en librerías

Las librerías deben retornar errores, no hacer panic. Reserva panic para situaciones verdaderamente excepcionales:

no_panic_in_libraries.go
// ❌ Malo (en una librería)
func divide(n, d int) int {
    if d == 0 {
        panic("división por cero")
    }
    return n / d
}

// ✅ Bueno
func divide(n, d int) (int, error) {
    if d == 0 {
        return 0, errors.New("división por cero")
    }
    return n / d, nil
}

6. Usar errors.Is y errors.As para verificación de errores

Usa estas funciones para verificar valores o tipos de errores específicos en errores envueltos y actúa en consecuencia:

errors_is_as_usage.go
// Verificar valores de errores específicos
if errors.Is(err, ErrUserNotFound) {
    // Manejar usuario no encontrado
}

// Extraer tipos de errores específicos
var validationErr *ValidationError
if errors.As(err, &validationErr) {
    // Manejar error de validación
}

Depuración y Logging de Errores

El logging efectivo es crucial para diagnosticar problemas en producción. Veamos cómo trazar errores de forma efectiva para mejorar la experiencia de depuración.

Structured logging

Usa structured logging para hacer la depuración de errores más fácil:

structured_logging.go
package main

import (
    "errors"
    "log/slog"
    "os"
)

func processUser(userID string) error {
    return errors.New("conexión a base de datos falló")
}

func main() {
    // Crear logger estructurado
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

    // Escenario de error de ejemplo
    userID := "Aitana"
    err := processUser(userID)
    if err != nil {
        logger.Error("Error al procesar usuario de blog.marcnuri.com",
            "user_id", userID,
            "error", err.Error(),
            "operation", "user_processing",
        )
    }
}

En este ejemplo, usamos el paquete slog de Go para crear un logger estructurado que emite trazas JSON. Cuando ocurre un error en processUser, trazamos el error junto con contexto relevante como el ID de usuario y el nombre de la operación.

Este enfoque estructurado hace más fácil filtrar y analizar logs, especialmente en entornos de producción.

La salida (formateada) se verá así:

{
  "time":"2015-10-21T04:29:00Z",
  "level":"ERROR",
  "msg":"Error al procesar usuario de blog.marcnuri.com",
  "user_id":"Aitana",
  "error":"conexión a base de datos falló",
  "operation":"user_processing"
}

Stack traces de errores

En general, puedes confiar en wrapping y contexto para rastrear errores. Sin embargo, en escenarios complejos, capturar stacktraces puede ayudar a encontrar la causa raíz de errores.

Stack-tracing no es soportado directamente en la librería estándar de Go. En el ejemplo abajo, implementamos un tipo de error personalizado sencillo que captura el stack trace cuando el error es creado.

stack_trace.go
package main

import (
    "fmt"
    "runtime"
)

// ErrorWithStack wrappea un error con información de stack trace
type ErrorWithStack struct {
    Err   error
    Stack []uintptr
}

func (e *ErrorWithStack) Error() string {
    return e.Err.Error()
}

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

// StackTrace retorna el stack trace como strings
func (e *ErrorWithStack) StackTrace() []string {
    frames := runtime.CallersFrames(e.Stack)
    var trace []string

    for {
        frame, more := frames.Next()
        trace = append(trace, fmt.Sprintf("%s:%d %s", frame.File, frame.Line, frame.Function))
        if !more {
            break
        }
    }

    return trace
}

// NewErrorWithStack crea un error con stack trace
func NewErrorWithStack(err error) *ErrorWithStack {
    const depth = 32
    var pcs [depth]uintptr
    n := runtime.Callers(2, pcs[:])

    return &ErrorWithStack{
        Err:   err,
        Stack: pcs[0:n],
    }
}

func functionA() error {
    return functionB()
}

func functionB() error {
    return functionC()
}

func functionC() error {
    return NewErrorWithStack(errors.New("algo salió mal"))
}

func main() {
    err := functionA()
    if err != nil {
        fmt.Printf("Error: %v\n", err)

        var stackErr *ErrorWithStack
        if errors.As(err, &stackErr) {
            fmt.Println("\nStack trace:")
            for _, frame := range stackErr.StackTrace() {
                fmt.Printf("  %s\n", frame)
            }
        }
    }
}

En este ejemplo, definimos un tipo ErrorWithStack que captura el stack trace cuando el error es creado usando la función runtime.Callers.

En la función main, simulamos una serie de llamadas de función que tarde o temprano retornan un error envuelto con un stack trace. Cuando detectamos que el error es de tipo ErrorWithStack, imprimimos el stack trace para ayudar a diagnosticar dónde se originó el error.

Precaución

Los stacktraces pueden ser costosos de capturar, así que resérvalos para builds de depuración o fallos críticos.

La salida se verá así:

stack_trace_output.txt
Error: algo salió mal
Stack trace:
  /home/blog.marcnuri.com/stack_trace.go:34 main.functionC
  /home/blog.marcnuri.com/stack_trace.go:29 main.functionB
  /home/blog.marcnuri.com/stack_trace.go:24 main.functionA
  /home/blog.marcnuri.com/stack_trace.go:40 main.main
  /usr/local/go/src/runtime/proc.go:225 runtime.main
  /usr/local/go/src/runtime/asm_amd64.s:1371 runtime.goexit

Conclusión

En este artículo, te he mostrado cómo manejar errores explícitamente, crear tipos de errores personalizados, envolver y extraer errores, y aplicar estos patrones en varios contextos como aplicaciones web, operaciones de base de datos, y programación concurrente.

El manejo efectivo de errores es crucial para construir aplicaciones Go robustas. Siguiendo estas pautas, la filosofía y mejores prácticas de manejo de errores de Go, podrás escribir código Go que maneje errores sin problema, haciendo tus aplicaciones más resilientes y fáciles de depurar.

Recuerda, el manejo de errores es una decisión de diseño, moldea la estabilidad de tu aplicación y la experiencia del próximo desarrollador que mantenga tu código.

También te puede interesar

  • Cómo Inicializar un Nuevo Proyecto Go con Módulos
  • Patrones de Concurrencia en Go: Goroutines y Channels
  • Cómo preparar y desmontar tests unitarios en Go
Twitter iconFacebook iconLinkedIn iconPinterest iconEmail icon

Navegador de artículos
Lanzando GitHub Actions entre distintos repositoriosPatrones de Concurrencia en Go: Goroutines y Channels
© 2007 - 2025 Marc Nuri