Buenas Prácticas para el Manejo de Errores en Go
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:
- Los errores son valores: se comportan como valores de retorno normales.
- Manejo explícito: debes verificarlos y actuar sobre ellos.
- Sin flujo de control oculto: no hay bloques try/catch que alteren la ejecución de forma inesperada.
- 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
:
type error interface {
Error() string
}
Un error básico puede crearse con errors.New()
:
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:
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:
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:
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).
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
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:
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:
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:
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:
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:
// ❌ 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.
// ❌ 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:
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:
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:
// ❌ 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:
// 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:
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.
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í:
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.