Manejo de Errores
Go maneja errores de forma explicita mediante valores. No hay excepciones ni try/catch. Los errores son valores que implementan la interfaz error, y se retornan como cualquier otro valor.
La Interfaz error
La interfaz error es la mas simple de Go:
type error interface {
Error() string
}
Cualquier tipo con un metodo Error() string es un error valido.
Crear Errores
Con errors.New
Para errores simples sin formato:
import "errors"
func dividir(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division por cero")
}
return a / b, nil
}
func main() {
resultado, err := dividir(10, 0)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println(resultado)
}
Con fmt.Errorf
Para errores con contexto formateado:
func buscarUsuario(id int) (*Usuario, error) {
if id <= 0 {
return nil, fmt.Errorf("id invalido: %d", id)
}
// buscar en BD...
return nil, fmt.Errorf("usuario con id %d no encontrado", id)
}
El Patron if err != nil
Es el patron fundamental de manejo de errores en Go:
func leerConfig(ruta string) (*Config, error) {
data, err := os.ReadFile(ruta)
if err != nil {
return nil, fmt.Errorf("leyendo config: %w", err)
}
var cfg Config
err = json.Unmarshal(data, &cfg)
if err != nil {
return nil, fmt.Errorf("parseando config: %w", err)
}
return &cfg, nil
}
Cada operacion que puede fallar se verifica inmediatamente. El flujo feliz avanza en linea recta.
Error Wrapping con %w
Desde Go 1.13, puedes envolver errores para agregar contexto sin perder el error original:
func abrirDB(dsn string) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("abrirDB: %w", err)
}
err = db.Ping()
if err != nil {
return nil, fmt.Errorf("abrirDB ping: %w", err)
}
return db, nil
}
El verbo %w (wrap) preserva la cadena de errores. Usa %v si no quieres que sea desenvuelto.
errors.Is y errors.As
errors.Is
Compara contra un error especifico, recorriendo la cadena de wrapping:
func leerArchivo(nombre string) ([]byte, error) {
data, err := os.ReadFile(nombre)
if err != nil {
return nil, fmt.Errorf("leerArchivo %s: %w", nombre, err)
}
return data, nil
}
func main() {
_, err := leerArchivo("noexiste.txt")
if err != nil {
// Verifica el error original a traves del wrapping
if errors.Is(err, os.ErrNotExist) {
fmt.Println("El archivo no existe")
} else {
fmt.Println("Error:", err)
}
}
}
errors.As
Extrae un tipo especifico de error de la cadena:
type ErrorValidacion struct {
Campo string
Mensaje string
}
func (e *ErrorValidacion) Error() string {
return fmt.Sprintf("%s: %s", e.Campo, e.Mensaje)
}
func registrar(nombre string) error {
if nombre == "" {
return fmt.Errorf("registrar: %w", &ErrorValidacion{
Campo: "nombre",
Mensaje: "es requerido",
})
}
return nil
}
func main() {
err := registrar("")
var ve *ErrorValidacion
if errors.As(err, &ve) {
fmt.Printf("Error de validacion en campo '%s': %s\n",
ve.Campo, ve.Mensaje)
}
}
| Funcion | Proposito | Compara por |
|---|---|---|
errors.Is(err, target) | Es este error exacto? | Valor/identidad |
errors.As(err, &target) | Es de este tipo? | Tipo |
Sentinel Errors
Son errores predefinidos como variables de paquete. Sirven como senales conocidas:
var (
ErrNoEncontrado = errors.New("recurso no encontrado")
ErrNoAutorizado = errors.New("no autorizado")
ErrYaExiste = errors.New("el recurso ya existe")
)
func obtenerProducto(id string) (*Producto, error) {
producto, existe := catalogo[id]
if !existe {
return nil, ErrNoEncontrado
}
return producto, nil
}
func main() {
_, err := obtenerProducto("xyz")
if errors.Is(err, ErrNoEncontrado) {
fmt.Println("Producto no existe, creando uno nuevo...")
}
}
Sentinel errors comunes en la stdlib:
| Error | Paquete | Significado |
|---|---|---|
io.EOF | io | Fin de lectura |
sql.ErrNoRows | database/sql | Query sin resultados |
os.ErrNotExist | os | Archivo no existe |
os.ErrPermission | os | Sin permisos |
context.Canceled | context | Contexto cancelado |
context.DeadlineExceeded | context | Timeout |
Errores Custom
Para errores con informacion estructurada, implementa la interfaz error:
type ErrorHTTP struct {
Codigo int
Mensaje string
URL string
}
func (e *ErrorHTTP) Error() string {
return fmt.Sprintf("HTTP %d en %s: %s", e.Codigo, e.URL, e.Mensaje)
}
func (e *ErrorHTTP) EsClientError() bool {
return e.Codigo >= 400 && e.Codigo < 500
}
func llamarAPI(url string) ([]byte, error) {
resp, err := http.Get(url)
if err != nil {
return nil, fmt.Errorf("llamarAPI: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, &ErrorHTTP{
Codigo: resp.StatusCode,
Mensaje: resp.Status,
URL: url,
}
}
return io.ReadAll(resp.Body)
}
func main() {
_, err := llamarAPI("https://api.ejemplo.com/datos")
if err != nil {
var httpErr *ErrorHTTP
if errors.As(err, &httpErr) {
if httpErr.EsClientError() {
fmt.Println("Error del cliente:", httpErr)
} else {
fmt.Println("Error del servidor:", httpErr)
}
} else {
fmt.Println("Error de red:", err)
}
}
}
Errores Multiples con errors.Join (Go 1.20+)
Combina varios errores en uno solo:
func validarFormulario(nombre, email string, edad int) error {
var errs []error
if nombre == "" {
errs = append(errs, errors.New("nombre es requerido"))
}
if !strings.Contains(email, "@") {
errs = append(errs, errors.New("email invalido"))
}
if edad < 18 {
errs = append(errs, fmt.Errorf("edad minima 18, recibido: %d", edad))
}
return errors.Join(errs...)
}
func main() {
err := validarFormulario("", "sinArroba", 15)
if err != nil {
fmt.Println("Errores de validacion:")
fmt.Println(err)
// nombre es requerido
// email invalido
// edad minima 18, recibido: 15
}
// errors.Is funciona con cada error individual
fmt.Println(errors.Is(err, nil)) // false
}
Buenas Practicas
1. Agrega contexto al propagar
// MAL: pierde informacion
if err != nil {
return err
}
// BIEN: agrega contexto
if err != nil {
return fmt.Errorf("guardando usuario %s: %w", u.Nombre, err)
}
2. Maneja el error solo una vez
// MAL: log + return (se duplica el manejo)
if err != nil {
log.Println("error:", err)
return err
}
// BIEN: o log o return, no ambos
if err != nil {
return fmt.Errorf("procesando pedido: %w", err)
}
3. No ignores errores
// MAL: error silenciado
json.Unmarshal(data, &config)
// BIEN: siempre verifica
if err := json.Unmarshal(data, &config); err != nil {
return fmt.Errorf("parseando config: %w", err)
}
4. Usa sentinel errors para flujo de control
for {
linea, err := lector.ReadString('\n')
if errors.Is(err, io.EOF) {
break // fin normal de lectura
}
if err != nil {
return fmt.Errorf("leyendo: %w", err)
}
procesar(linea)
}
5. Documenta los errores que retornas
// BuscarUsuario busca un usuario por ID.
// Retorna ErrNoEncontrado si el usuario no existe.
// Retorna ErrNoAutorizado si no hay permisos.
func BuscarUsuario(id string) (*Usuario, error) {
// ...
}
Resumen
| Concepto | Descripcion |
|---|---|
error interface | Error() string es todo lo que necesitas |
errors.New | Error simple sin formato |
fmt.Errorf("%w") | Error con contexto + wrapping |
errors.Is | Compara por valor en la cadena |
errors.As | Extrae por tipo en la cadena |
| Sentinel errors | Variables de error predefinidas |
errors.Join | Combina multiples errores (Go 1.20+) |