Lección 5 de 8. Go no tiene excepciones. Un error es un valor que se devuelve como cualquier otro, y comprobarlo es tu trabajo, línea a línea. Suena tedioso y lo es un poco; a cambio, leyendo una función sabes exactamente por dónde puede fallar.
El error es un valor
error es una interfaz de la biblioteca estándar con un solo método. Esto es toda su definición:
type error interface {
Error() string
}
Cualquier cosa con un método Error() string es un error. Por eso una función que puede fallar devuelve dos valores: el resultado y un error que vale nil cuando todo fue bien.
func comprar(unidades int, precio float64) error {
if unidades <= 0 {
return ErrSinStock
}
if precio < 0 {
return &ErrPrecioInvalido{Valor: precio}
}
return nil
}
El patrón que vas a escribir mil veces
if err := comprar(0, 10); err != nil {
return err // o registrarlo, o devolverlo con contexto
}
// a partir de aquí se da por hecho que todo fue bien
Tres cosas de esa forma. Primera: el if con inicialización que vimos en la lección 3, que deja err acotada. Segunda: el camino de error sale pronto, así que el flujo normal se queda sin anidar. Y tercera, la que más cuesta aceptar: no se ignora nunca. Si no te importa, hay que decirlo en voz alta con _ = f(), y eso se ve al leer.
Dos maneras de crear un error
var ErrSinStock = errors.New("sin stock") // fijo, comparable
return fmt.Errorf("no se puede dividir %v entre cero", a) // con datos dentro
El primero se llama error centinela: se declara una vez como variable de paquete y quien lo recibe puede compararlo. El segundo se crea al vuelo con los datos del momento.
Envolver: añadir contexto sin perder el original
Un error que sube por cinco funciones y solo dice «sin stock» no ayuda a nadie. Pero reemplazarlo por otro pierde la información de abajo. La solución es envolverlo, con el verbo %w:
func procesarPedido(unidades int, precio float64) error {
if err := comprar(unidades, precio); err != nil {
return fmt.Errorf("procesando el pedido: %w", err)
}
return nil
}
Ahora el texto lleva las dos capas y, además, la de dentro sigue ahí para poder preguntarle:
err := procesarPedido(0, 10)
fmt.Println(err)
fmt.Println("¿es ErrSinStock?", errors.Is(err, ErrSinStock))
salidaprocesando el pedido: sin stock ¿es ErrSinStock? true
%v el error de dentro se convierte en texto y se pierde: el mensaje se ve igual, pero errors.Is devuelve false y ya no hay manera de saber qué pasó. Usa %w siempre que quieras que el de arriba pueda seguir preguntando.errors.Is y errors.As
Son las dos formas de interrogar una cadena de errores, y se usan para cosas distintas.
errors.Is responde «¿hay este error concreto ahí dentro?». Sirve para los centinelas:
_, err := os.Open("no_existe.txt")
fmt.Println(err)
fmt.Println("¿es ErrNotExist?", errors.Is(err, os.ErrNotExist))
salidaopen no_existe.txt: no such file or directory ¿es ErrNotExist? true
errors.As responde «¿hay un error de este tipo, y me lo das para mirar sus datos?». Sirve cuando el error lleva información:
type ErrPrecioInvalido struct {
Valor float64
}
func (e *ErrPrecioInvalido) Error() string {
return fmt.Sprintf("precio inválido: %.2f", e.Valor)
}
err = procesarPedido(2, -5)
fmt.Println(err)
var ep *ErrPrecioInvalido
if errors.As(err, &ep) {
fmt.Println("el precio que falló era", ep.Valor)
}
salidaprocesando el pedido: precio inválido: -5.00 el precio que falló era -5
Fíjate en que a errors.As se le pasa la dirección de una variable del tipo que buscas: ahí es donde deja el error encontrado.
defer: lo que hay que hacer sí o sí
defer aplaza una llamada hasta que la función termine, salga por donde salga. Es lo que en otros lenguajes resuelve un finally.
func escribir(ruta string, texto string) error {
f, err := os.Create(ruta)
if err != nil {
return fmt.Errorf("creando %s: %w", ruta, err)
}
defer f.Close() // pase lo que pase a partir de aquí, se cierra
if _, err := f.WriteString(texto); err != nil {
return fmt.Errorf("escribiendo en %s: %w", ruta, err)
}
return nil
}
La costumbre es ponerlo justo después de comprobar que el recurso se abrió. Así el cierre está al lado de la apertura y no hay que acordarse de él en cada return.
Si hay varios, salen en orden inverso, como una pila:
for i := 1; i <= 3; i++ {
defer fmt.Println("defer", i)
}
fmt.Println("final de la función")
salidafinal de la función defer 3 defer 2 defer 1
panic: lo que casi nunca vas a usar
Un panic aborta el programa. Lo lanza el propio lenguaje cuando pasa algo imposible —un índice fuera de rango, un puntero nulo—, y se puede interceptar con recover dentro de un defer:
defer func() {
if r := recover(); r != nil {
fmt.Println("recuperado de:", r)
}
}()
var s []int
_ = s[5]
salidarecuperado de: runtime error: index out of range [5] with length 0 el programa sigue vivo
panic es para fallos de los que no se puede seguir: una configuración imposible al arrancar, un estado que no debería existir. Un archivo que no está o una conexión que falla no son eso, son cosas previstas, y para eso están los errores. Si estás usando recover para controlar el flujo normal, has traído las excepciones a un lenguaje que decidió no tenerlas.Lo que hay que llevarse
| Para | Usa |
|---|---|
| Crear un error fijo y comparable | errors.New en una variable ErrAlgo |
| Crear uno con datos del momento | fmt.Errorf("...%v...", x) |
| Añadir contexto conservando el de dentro | fmt.Errorf("...: %w", err) |
| Preguntar si es un error concreto | errors.Is(err, ErrAlgo) |
| Sacar un error con datos | errors.As(err, &miTipo) |
| Garantizar una limpieza | defer, justo tras abrir el recurso |
| Abortar ante algo imposible | panic, y con mucha moderación |
Todo el código de esta lección se compiló y ejecutó con Go 1.25 antes de publicarla. El error de os.Open es el real del sistema, y el panic se provocó de verdad accediendo a un índice fuera de rango.