Módulo 02: Composición, Interfaces Implícitas & Manejo de Errores
Diseña sin herencia: structs, interfaces implícitas y errores como valores.
Fase -1 & 0: Fundamentos, terminal y mentalidad Go · Lección 5 de 20
🎯 Objetivos
- Modelar con structs y métodos en lugar de jerarquías de clases.
- Definir interfaces pequeñas y satisfacerlas implícitamente.
- Usar aserción de tipos con seguridad.
- Envolver, comparar y extraer errores con
fmt.Errorf,errors.Isyerrors.As.
1. Structs y métodos
Go no tiene clases. Tiene tipos con métodos asociados.
type Tarea struct {
ID int
Titulo string
Hecha bool
}
// Método con receptor de valor: trabaja sobre una copia.
func (t Tarea) Resumen() string {
estado := " "
if t.Hecha {
estado = "x"
}
return fmt.Sprintf("[%s] %s", estado, t.Titulo)
}
// Método con receptor de puntero: puede modificar el original.
func (t *Tarea) Completar() { t.Hecha = true }
Reglas prácticas para elegir receptor:
- Si el método modifica el receptor → puntero.
- Si la struct es grande o contiene un
sync.Mutex→ puntero. - No mezcles receptores de valor y puntero en un mismo tipo.
- Un constructor es solo una función por convención:
func NuevaTarea(titulo string) *Tarea.
Cualquier tipo definido por ti puede tener métodos, no solo structs:
type Prioridad int
const (
Baja Prioridad = iota // 0
Media // 1
Alta // 2
)
func (p Prioridad) String() string {
return [...]string{"baja", "media", "alta"}[p]
}
(String() implementa la interfaz fmt.Stringer: fmt.Println(Alta) imprime alta.)
2. Composición en lugar de herencia
Para reutilizar comportamiento, incrusta (embed) un tipo dentro de otro:
type Auditoria struct {
CreadoEn time.Time
CreadoPor string
}
func (a Auditoria) Descripcion() string {
return a.CreadoPor + " el " + a.CreadoEn.Format("2006-01-02")
}
type Proyecto struct {
Auditoria // incrustado: sus campos y métodos se "promueven"
Nombre string
}
p := Proyecto{Nombre: "Tareas", Auditoria: Auditoria{CreadoPor: "ana"}}
fmt.Println(p.CreadoPor) // acceso directo
fmt.Println(p.Descripcion()) // método promovido
Esto no es herencia: Proyecto no “es un” Auditoria, simplemente tiene uno y expone sus miembros. No hay polimorfismo por jerarquía, no hay super. Eso hace el código más fácil de seguir.
3. Interfaces implícitas
Una interfaz define un conjunto de métodos. Un tipo la satisface automáticamente si los implementa; no existe implements.
type Notificador interface {
Notificar(mensaje string) error
}
type Email struct{ Direccion string }
func (e Email) Notificar(m string) error {
fmt.Printf("Enviando a %s: %s\n", e.Direccion, m)
return nil
}
type Consola struct{}
func (Consola) Notificar(m string) error {
fmt.Println("LOG:", m)
return nil
}
// Esta función no sabe nada de Email ni Consola.
func Avisar(n Notificador, texto string) error {
return n.Notificar(texto)
}
Se le llama a veces duck typing estático: “si camina como pato…”, pero verificado en compilación. Consecuencias valiosas:
- Las interfaces las define quien las consume, no quien implementa. Puedes crear una interfaz para un tipo de una biblioteca ajena sin modificarla.
- Interfaces pequeñas (1–3 métodos) son las mejores. Ejemplos de la biblioteca estándar:
io.Reader(Read),io.Writer(Write),error(Error() string),fmt.Stringer,http.Handler(ServeHTTP). - “Acepta interfaces, devuelve structs.”
- Facilitan las pruebas: sustituyes una base de datos real por un doble en memoria (Módulo 03).
Para comprobar en compilación que un tipo cumple una interfaz:
var _ Notificador = Email{} // no compila si Email deja de implementarla
La interfaz vacía any (interface{}) acepta cualquier valor; úsala con moderación porque pierdes el sistema de tipos.
Aserción de tipos y type switch
var v any = "hola"
s, ok := v.(string) // forma segura
if ok { fmt.Println(len(s)) }
n := v.(int) // sin "ok": hace panic si no es int
switch x := v.(type) {
case string: fmt.Println("texto", x)
case int: fmt.Println("entero", x)
case nil: fmt.Println("nada")
default: fmt.Printf("otro: %T\n", x)
}
También puedes preguntar si un valor tiene además otro comportamiento: if c, ok := w.(io.Closer); ok { c.Close() }.
Trampa conocida: una interfaz es nil solo si tanto su tipo como su valor son nil. Devolver un puntero nil concreto dentro de una interfaz error produce un error != nil. Devuelve siempre nil literal cuando no hay error.
4. Errores como valores
error es una interfaz mínima:
type error interface { Error() string }
El patrón idiomático:
func Buscar(id int) (*Tarea, error) {
// ...
return nil, ErrNoEncontrada
}
t, err := Buscar(7)
if err != nil {
return fmt.Errorf("cargando tarea: %w", err) // añade contexto y conserva el original
}
Sin excepciones, sin try/catch: el flujo de errores es visible en el código. Sí es verbosa, pero nunca dudas qué puede fallar.
Errores centinela
var ErrNoEncontrada = errors.New("tarea no encontrada")
Errores como tipos
Cuando el error necesita datos adicionales:
type ErrValidacion struct {
Campo string
Mensaje string
}
func (e *ErrValidacion) Error() string {
return e.Campo + ": " + e.Mensaje
}
Envolver, comparar y extraer
%w en fmt.Errorf crea una cadena de errores:
func cargar(id int) error {
if err := buscarEnBD(id); err != nil {
return fmt.Errorf("cargar(%d): %w", id, err)
}
return nil
}
err := cargar(7)
// ¿Algún error de la cadena ES este valor?
if errors.Is(err, ErrNoEncontrada) {
// responder 404
}
// ¿Algún error de la cadena ES de este tipo? Si sí, lo extrae.
var ev *ErrValidacion
if errors.As(err, &ev) {
fmt.Println("campo inválido:", ev.Campo)
}
Nunca compares con == errores que pueden venir envueltos; usa errors.Is. Si necesitas combinar varios errores, errors.Join(err1, err2) los agrupa y errors.Is/As sigue funcionando.
Buenas prácticas
- Maneja el error una sola vez: o lo registras o lo devuelves, no ambas cosas.
- Añade contexto al propagarlo, en minúscula y sin puntuación final (
"abriendo config: %w"). - No uses
panicpara el flujo normal.panices para errores de programación irrecuperables; en un servidor, un middleware de recuperación lo captura (Módulo 04). - Define errores de dominio (
ErrNoEncontrada) en el paquete del dominio y traduce a HTTP en el borde:ErrNoEncontrada → 404.
🧪 Práctica
Define una interfaz Notificador con dos implementaciones (email y consola). Crea un error centinela ErrNoEncontrado, envuélvelo con %w y compruébalo con errors.Is en el llamador. Añade un error tipado ErrValidacion y recupéralo con errors.As.
Ver solución
package main
import (
"errors"
"fmt"
)
var ErrNoEncontrado = errors.New("no encontrado")
type ErrValidacion struct{ Campo string }
func (e *ErrValidacion) Error() string { return "campo inválido: " + e.Campo }
type Notificador interface{ Notificar(string) error }
type Email struct{ Para string }
func (e Email) Notificar(m string) error {
if e.Para == "" {
return &ErrValidacion{Campo: "para"}
}
fmt.Printf("email a %s: %s\n", e.Para, m)
return nil
}
type Consola struct{}
func (Consola) Notificar(m string) error { fmt.Println("consola:", m); return nil }
func buscar(id int) error {
if id != 1 {
return fmt.Errorf("buscar(%d): %w", id, ErrNoEncontrado)
}
return nil
}
func main() {
for _, n := range []Notificador{Email{Para: "a@b.c"}, Consola{}, Email{}} {
if err := n.Notificar("hola"); err != nil {
var ev *ErrValidacion
if errors.As(err, &ev) {
fmt.Println("validación falló en:", ev.Campo)
}
}
}
err := buscar(5)
fmt.Println(err) // buscar(5): no encontrado
fmt.Println(errors.Is(err, ErrNoEncontrado)) // true
}
⚠️ Errores comunes
- Mezclar receptores de valor y puntero en el mismo tipo.
- Comparar errores con
==después de envolverlos. - Registrar y además devolver el mismo error (duplica los logs).
- Declarar interfaces enormes “por si acaso”.
- Devolver un puntero nil tipado como
error.
📌 Resumen
- Tipos + métodos reemplazan a las clases; la composición reemplaza a la herencia.
- Las interfaces se satisfacen implícitamente y conviene que sean pequeñas y definidas por el consumidor.
- Los errores son valores: se devuelven, se envuelven con
%wy se inspeccionan conerrors.Is/errors.As.
✅ Autoevaluación
- ¿Qué diferencia hay entre promover métodos por embedding y heredar?
- ¿Por qué se dice que las interfaces de Go son implícitas?
- ¿Qué ocurre con
v.(int)sivcontiene unstring? ¿Cómo lo evitas? - ¿Cuándo usarías
errors.Isy cuándoerrors.As? - ¿Por qué conviene que el consumidor defina la interfaz?
- Puedo explicar con mis palabras cada objetivo de esta lección.
- Mi práctica pasa
go vetygolangci-lint. - Sé qué parte me costó más y la repasaré.