Manejo de errores en Go: el idiomático if err != nil y por qué es una buena práctica
Introducción
En el desarrollo de software, los errores son inevitables. Ya sea por fallos de red, archivos inexistentes o datos inesperados, una aplicación robusta debe anticiparlos y reaccionar de forma adecuada. En Go, el manejo de errores se basa en un enfoque simple y explícito: cada función que puede fallar devuelve un valor adicional de tipo error. El idiomático if err != nil se vuelve entonces la forma estándar de comprobar si algo salió mal.
Este artículo está dirigido a programadores que ya conocen los conceptos básicos de Go (sintaxis, estructuras de control, funciones) y quieren profundizar en cómo tratar los errores de forma idiomática, segura y mantenible. Al terminar de leer, comprenderás:
- Por qué Go eligió representar los errores como valores y no como excepciones.
- Cómo usar el patrón
if err != nilde manera efectiva. - Qué técnicas existen para envolver, inspeccionar y personalizar errores.
- Cuándo es apropiado usar
panicyrecovery por qué generalmente se evita. - Buenas prácticas y errores comunes que debes evitar.
A lo largo del artículo encontrarás ejemplos completos, con su salida esperada, para que puedas copiar y ejecutar cada fragmento en tu entorno o en el playground de Go.
¿Qué es el tipo error en Go?
En la biblioteca estándar, el tipo error se define como una interfaz con un único método:
type error interface {
Error() string
}Cualquier tipo que implemente este método puede ser usado como un error. La simplicidad de esta definición permite que cualquier desarrollador cree su propio tipo de error añadiendo comportamiento adicional (códigos, contexto, stack traces, etc.) sin romper la compatibilidad con el código existente.
La ventaja de tratar los errores como valores es que pueden ser inspeccionados, comparados, modificados y propagados usando las mismas herramientas que usamos para cualquier otro dato. No hay necesidad de mecanismos de control de flujo externos como try/catch; el flujo sigue siendo lineal y fácil de seguir.
El idiomático if err != nil
Cuando una función firma incluye un último retorno de tipo error, el llamador tiene la responsabilidad de verificar ese valor. La forma más directa y recomendada es:
if err != nil {
// manejar el error
}Este bloque se ejecuta solo cuando err no es nil, es decir, cuando la función indicó que algo salió mal. Si el valor es nil, la operación fue exitosa y el programa continúa normalmente.
¿Por qué está bien? Porque obliga al programador a enfrentar explícitamente la posibilidad de fallo. No hay excepciones silenciosas que se propaguen por la pila de llamadas sin ser vistas; cada posible punto de falla debe ser considerado. Además, el código permanece legible: la ruta feliz (el caso de éxito) queda al mismo nivel de indentación que el manejo de error, sin anidamientos profundos.
Ejemplo básico: función que devuelve error
Veamos una función que intenta abrir un archivo y devuelve su contenido o un error.
package main
import (
"fmt"
"os"
)
func leerArchivo(nombre string) (string, error) {
data, err := os.ReadFile(nombre)
if err != nil {
return "", fmt.Errorf("no se pudo leer %s: %w", nombre, err)
}
return string(data), nil
}
func main() {
contenido, err := leerArchivo("ejemplo.txt
)
if err != nil {
fmt.Printf("Error: %v
", err)
return
}
fmt.Println("Contenido del archivo:" + contenido)
}Salida esperada (si el archivo ejemplo.txt no existe):
Error: no se pudo leer ejemplo.txt: open ejemplo.txt: no such file or directorySi creas el archivo con algún contenido, la salida será:
Contenido del archivo:
<hola mundo>En este ejemplo, la función leerArchivo devuelve un error envuelto con fmt.Errorf y el verbo %w para preservar el error original. En main, verificamos if err != nil y actuamos en consecuencia.
Manejo de múltiples errores
Algunas funciones pueden devolver más de un valor además del error, por ejemplo, una fila de una base de datos o un par de resultados. El patrón sigue siendo el mismo: primero revisamos el error y, solo si es nil, utilizamos los otros valores.
package main
import (
"fmt"
)
func dividir(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("división por cero\))}
return a / b, nil
}
func main() {
resultado, err := dividir(10, 0)
if err != nil {
fmt.Printf("Error al dividir: %v
", err)
return
}
fmt.Printf("Resultado: %.2f
", resultado)
}Salida esperada:
Error al dividir: división por ceroNota cómo el mensaje de error es claro y directo, gracias al uso de fmt.Errorf.
Envolver errores con fmt.Errorf
A medida que las llamadas se apilan, es útil agregar contexto al error sin perder la información original. Desde Go 1.13, el verbo %w en fmt.Errorf permite envolver un error y crear una cadena de causalidad.
package main
import (
"fmt"
)
func consultaBD(id int) (string, error) {
// Simulamos un error de conexión
return "", fmt.Errorf("fallo al conectar a la base de datos: %w", errConexion)
}
var errConexion = fmt.Errorf("timeout de red\))}
func obtenerUsuario(id int) (string, error) {
nombre, err := consultaBD(id)
if err != nil {
return "", fmt.Errorf("no se pudo obtener usuario %d: %w", id, err)
}
return nombre, nil
}
func main() {
_, err := obtenerUsuario(42)
if err != nil {
fmt.Printf("Error final: %v
", err)
}
}Salida esperada:
Error final: no se pudo obtener usuario 42: fallo al conectar a la base de datos: timeout de redObserve cómo el error original (timeout de red) está preservado y puede ser inspeccionado más adelante usando errors.Is o errors.As.
Errores sentinel y errors.Is, errors.As
A veces necesitamos comparar un error con un valor conocido (por ejemplo, io.EOF). Para eso se usan los errores sentinel: variables de tipo error cuyo valor es estático y comparable con ==.
var ErrNotFound = errors.New("recurso no encontrado\))}Para verificar si un error coincide con un sentinel, se debe usar errors.Is en lugar de ==, ya que esto también recorre la cadena de errores envueltos.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("recurso no encontrado\)
func buscar(clave string) (string, error) {
return "", ErrNotFound
}
func main() {
_, err := buscar("clave\))}
if errors.Is(err, ErrNotFound) {
fmt.Println("El recurso solicitado no existe\))}
} else if err != nil {
fmt.Printf("Otro error: %v
", err)
}
}Salida esperada:
El recurso solicitado no existeerrors.As sirve para intentar convertir un error a un tipo específico, útil cuando se tienen errores personalizados con campos adicionales.
package main
import (
"errors"
"fmt"
)
type ErrorDeValidacion struct {
Campo string
Razón string
}
func (e *ErrorDeValidacion) Error() string {
return fmt.Sprintf("campo %s: %s", e.Campo, e.Razón)
}
func validarEdad(edad int) error {
if edad < 0 {
return &ErrorDeValidacion{Campo: "edad", Razón: "no puede ser negativa"}
}
return nil
}
func main() {
err := validarEdad(-5)
if err != nil {
var ev *ErrorDeValidacion
if errors.As(err, &ev) {
fmt.Printf("Error de validación en %s: %s
", ev.Campo, ev.Razón)
} else {
fmt.Printf("Error inesperado: %v
", err)
}
}
}Salida esperada:
Error de validación en edad: no puede ser negativaErrores personalizados
Cuando el contexto del error es rico (códigos, información de usuario, etc.), crear un tipo propio es la mejor opción. Implementar el método Error() satisface la interfaz error y permite añadir comportamientos extra como métodos para obtener códigos de error, mensajes en distintos idiomas, etc.
package main
import (
"fmt"
)
type ErrorDeEntrada struct {
Valor string
Problema string
}
func (e *ErrorDeEntrada) Error() string {
return fmt.Sprintf("valor %q no válido: %s", e.Valor, e.Problema)
}
func parsearEntero(s string) (int, error) {
var n int
_, err := fmt.Sscan(s, &n)
if err != nil {
return 0, &ErrorDeEntrada{Valor: s, Problema: "no es un número entero"}
}
return n, nil
}
func main() {
_, err := parsearEntero("doce\))}
if err != nil {
fmt.Printf("Error de parseo: %v
", err)
}
}Salida esperada:
Error de parseo: valor "doce" no válido: no es un número enteroEste enfoque permite que el llamador inspeccione los campos Valor y Problema mediante aserciones de tipo o errors.As si necesita tomar decisiones basadas en ellos.
Uso de panic y recover
Go proporciona el mecanismo de panic para situaciones que indican un error irrecuperable o una condición que debería impedir que el programa continúe (por ejemplo, una violación de invariante crítico). panic detiene la ejecución normal, comienza a desenrollar la pila ejecutando las llamadas diferidas (defer) y, si no hay un recover, termina el programa con un mensaje de error y el stack trace.
recover solo funciona dentro de una función diferida y captura el valor pasado a panic, permitiendo que el programa retome la ejecución normalmente.
Cuando usar panic
- Detectar una inconsistencia lógica que indique un bug (por ejemplo, recibir un
nildonde nunca debería ser posible). - En funciones de inicialización donde un fallo hace que el programa no pueda cumplir su propósito.
Cuando evitar panic
- Para errores esperables como entrada de usuario, fallos de red o archivos faltantes. En esos casos, devolver un
errores la forma correcta.
Ejemplo de uso adecuado de panic:
package main
import (
"fmt"
)
func calcularPromedio(nums []float64) float64 {
if len(nums) == 0 {
panic("calcularPromedio llamado con slice vacío\)
}
sum := 0.0
for _, v := range nums {
sum += v
}
return sum / float64(len(nums))
}
func main() {
defer func() {
if r := recover(); r != nil {
fmt.Printf("Recuperado de panic: %v
", r)
}
}()
prom := calcularPromedio([]float64{})
fmt.Printf("Promedio: %.2f
", prom)
}Salida esperada:
Recuperado de panic: calcularPromedio llamado con slice vacíoEn este caso, el defer captura el panic y el programa continúa, mostrando el mensaje de recuperación. Si elimináramos el defer, el programa abortaría con un stack trace.
Buenas prácticas
Siempre revisa el error inmediatamente después de la llamada.
No asumas que una función no puede fallar; verificaerrantes de usar cualquier otro valor de retorno.No ignores errores con
_salvo que realmente no importe.
Si decides descartar un error, comenta por qué es seguro hacerlo.Envuelve errores con contexto usando
%wcuando vayas a agregar información útil para depurar.
Evita crear mensajes de error que oculten la causa original.Prefiere errores sentinel o tipos personalizados para condiciones específicas.
Esto facilita que el llamador tome decisiones diferentes según el tipo de fallo.Usa
errors.Isyerrors.Asen lugar de comparaciones directas.
Así trabajas con cadenas de errores envueltos y con tipos personalizados de forma segura.Mantén los mensajes de error claros y en el idioma de los usuarios si van a ser mostrados directamente; para uso interno, sé conciso y técnico.
No uses
panicpara control de flujo ordinario.
Resérvalo para situaciones realmente excepcionales que indiquen un bug.Documenta los posibles errores que tu función puede devolver en los comentarios o en la documentación generada por
godoc.
Errores comunes
- Olvidar chequear el error: lleva a
nil pointer dereferenceo a usar valores sin sentido. - Sobrescribir el error original al envolverlo sin
%w, perdiendo la capacidad de comparar conerrors.Is. - Devolver errores genéricos como
errors.New("algo falló\)sin contexto, dificultando la depuración. - Usar
panicpara manejar errores de entrada; esto termina el programa inesperadamente y proporciona mala experiencia al usuario. - Ignorar errores en goroutines: si una goroutine falla y no se registra, el problema puede pasar desapercibido.
- No liberar recursos (como archivos o conexiones) cuando ocurre un error antes del final de la función.
Conclusión
El manejo de errores en Go, centrado en el tipo error y el idiomático if err != nil, es una de las características que hacen al lenguaje predecible y fácil de razonar. Al tratar los errores como valores explícitos, Go obliga al desarrollador a enfrentar cada punto de posible fallo, lo que resulta en código más seguro y más sencillo de mantener.
A lo largo de este artículo vimos cómo:
- Declarar y devolver errores usando la interfaz estándar.
- Aplicar el patrón
if err != nilde forma inmediata y legible. - Envolver errores con
%wpara agregar contexto sin perder la información original. - Comparar y extraer errores específicos con
errors.Isyerrors.As. - Crear tipos de error personalizados cuando se necesita más detalle.
- Utilizar
panicyrecoverúnicamente en situaciones realmente excepcionales. - Seguir buenas prácticas y evitar los errores más frecuentes.
Con estas herramientas, estás listo para escribir aplicaciones Go que manejen los fallos de forma elegante, informativa y sin sorpresas. La próxima vez que escribas una función que pueda fallar, recuerda: el if err != nil no es solo una convención, es la base de la confiabilidad en Go.
¿Te quedó alguna duda? Deja un comentario o prueba modificar los ejemplos para ver cómo cambian los mensajes de error. La práctica constante es la mejor forma de interiorizar estos patrones.
DUGLAS MORENO
Comentarios (0)
Sé el primero en comentar.