Go 1.13 introdujo el envoltorio de errores con %w y las funciones errors.Is y errors.As, que permiten inspeccionar errores en cadena sin perder contexto. Con Go 1.20 llegó errors.Join para combinar múltiples errores en uno solo. Entender este modelo es fundamental para escribir código Go idiomático.
Envolver errores con fmt.Errorf %w
%w crea un error que envuelve al original, añadiendo contexto sin perder el tipo ni el valor del error interno:
package main
import (
"errors"
"fmt"
)
var ErrNoEncontrado = errors.New("no encontrado")
func buscarUsuario(id int) error {
if id <= 0 {
return fmt.Errorf("buscarUsuario: id inválido %d", id)
}
if id == 99 {
return fmt.Errorf("buscarUsuario(id=%d): %w", id, ErrNoEncontrado)
}
return nil
}
func main() {
err := buscarUsuario(99)
fmt.Println(err) // buscarUsuario(id=99): no encontrado
}
errors.Is: comparar errores en la cadena
errors.Is recorre la cadena de errores envueltos buscando uno que coincida. Sustituye completamente la comparación directa con ==:
err := buscarUsuario(99)
// Comparación directa: falla porque err no ES ErrNoEncontrado, solo lo envuelve
fmt.Println(err == ErrNoEncontrado) // false
// errors.Is: recorre la cadena y lo encuentra
fmt.Println(errors.Is(err, ErrNoEncontrado)) // true
// Patrón idiomático para manejar casos específicos
if errors.Is(err, ErrNoEncontrado) {
fmt.Println("el usuario no existe, creando uno nuevo...")
}
errors.As: extraer el tipo de error
errors.As recorre la cadena buscando un error del tipo especificado y lo asigna a la variable destino:
type ErrorValidacion struct {
Campo string
Mensaje string
}
func (e *ErrorValidacion) Error() string {
return fmt.Sprintf("validación fallida en %s: %s", e.Campo, e.Mensaje)
}
func validarEmail(email string) error {
if !strings.Contains(email, "@") {
return &ErrorValidacion{Campo: "email", Mensaje: "falta el símbolo @"}
}
return nil
}
func procesarFormulario(email string) error {
if err := validarEmail(email); err != nil {
return fmt.Errorf("formulario inválido: %w", err)
}
return nil
}
func main() {
err := procesarFormulario("no-es-un-email")
var errVal *ErrorValidacion
if errors.As(err, &errVal) {
fmt.Printf("campo: %s, problema: %sn", errVal.Campo, errVal.Mensaje)
}
}
Implementar Unwrap en tipos propios
Para que tus errores personalizados participen en la cadena de errors.Is y errors.As, implementa el método Unwrap:
type ErrorBD struct {
Operacion string
Original error
}
func (e *ErrorBD) Error() string {
return fmt.Sprintf("BD[%s]: %v", e.Operacion, e.Original)
}
func (e *ErrorBD) Unwrap() error {
return e.Original // expone el error interno
}
var ErrConexionPerdida = errors.New("conexión perdida")
func consultarBD() error {
return &ErrorBD{
Operacion: "SELECT",
Original: ErrConexionPerdida,
}
}
func main() {
err := consultarBD()
fmt.Println(errors.Is(err, ErrConexionPerdida)) // true, gracias a Unwrap
}
errors.Join: combinar múltiples errores (Go 1.20)
errors.Join combina varios errores en uno solo. errors.Is y errors.As inspeccionan todos los errores incluidos:
func validarPedido(p Pedido) error {
var errs []error
if p.Total <= 0 {
errs = append(errs, errors.New("el total debe ser positivo"))
}
if p.UsuarioID == 0 {
errs = append(errs, errors.New("el usuario es obligatorio"))
}
if len(p.Items) == 0 {
errs = append(errs, errors.New("el pedido debe tener al menos un ítem"))
}
return errors.Join(errs...) // nil si errs está vacío
}
func main() {
err := validarPedido(Pedido{})
if err != nil {
fmt.Println(err)
// el total debe ser positivo
// el usuario es obligatorio
// el pedido debe tener al menos un ítem
}
}
Errores centinela vs tipos propios
- Usa errores centinela (
var ErrX = errors.New("...")) cuando solo importa si el error ocurrió, no los detalles. - Usa tipos propios cuando necesitas campos adicionales como el código HTTP, el campo que falló o la operación que se estaba ejecutando.
- Nunca uses
panicpara errores recuperables: reserva el panic para bugs del programa, no para entradas inválidas del usuario.
