Interfaces en Go: La verdadera magia que cambia cómo programas

Interfaces en Go: La verdadera magia que cambia cómo programas

Una guía exhaustiva sobre interfaces en Go: qué son, cómo funcionan, cómo se crean, satisfacción implícita, composición, casos reales, y por qué Go rompe el paradigma de otros lenguajes. Desde novatos hasta expertos.

Por Omar Flores

Existe un momento en la carrera de todo desarrollador Go cuando algo “click” en tu cabeza. No es cuando aprendes la sintaxis. No es cuando haces tu primer programa. Es cuando finalmente entiendes las interfaces. Y cuando ese momento llega, tu forma de escribir código cambia para siempre.

He visto desarrolladores de Java llegar a Go y quedar confundidos durante semanas. “¿Dónde están mis clases base? ¿Cómo declaro que implemento una interfaz? ¿Por qué mi código que implementa todos los métodos de una interfaz simplemente… funciona sin declararlo?”

Luego, después de luchar durante semanas, algo cambia. De repente entienden que Go no solo tiene una forma diferente de hacer interfaces. Go tiene una filosofía completamente diferente sobre qué son las interfaces, para qué sirven, y cómo deberían funcionar.

Este cambio de mentalidad es tan profundo que muchos desarrolladores Go dicen que es lo mejor que les pasó a su carrera de programación. No es exageración. Las interfaces en Go son tan poderosas, tan elegantes, y tan simples, que te hacen cuestionar por qué otros lenguajes las hacen tan complicadas.

Este artículo es una guía exhaustiva sobre interfaces en Go 1.25. No es una introducción rápida. Es una exploración profunda que te llevará desde “¿qué es una interfaz?” hasta entender cómo usarlas para resolver problemas complejos de arquitectura, testing, y diseño de software. Veremos código real, casos de uso reales, y explicaré no solo el cómo, sino el por qué Go diseñó las interfaces de esta manera.


Parte 1: Las Bases - Qué Son Las Interfaces

1.1 La Definición Más Simple Posible

Una interfaz es un contrato. Dice: “Si tu tipo tiene estos métodos, puede hacer esto.”

Eso es todo. No es magia. No es complejo. Es un contrato.

Veamos la interfaz más famosa de Go:

type Reader interface {
    Read(p []byte) (n int, err error)
}

Esta interfaz dice: “Si tu tipo tiene un método Read que recibe un slice de bytes y devuelve un int y un error, entonces es un Reader.”

Eso es. Nada de “implements”, nada de “extends”, nada de herencia. Solo: tienes el método, eres un Reader.

1.2 Por Qué Esto Es Revolucionario

En Java, si quieres que tu clase implemente una interfaz, escribes:

public class MiClase implements MiInterfaz {
    // Tu clase declara explícitamente que implementa MiInterfaz
}

Esto parece útil. Es explícito. Claro. Pero mira lo que sucede: tu código ahora depende de la interfaz. Si la interfaz vive en otra librería, tu código depende de esa librería. Si alguien cambia la interfaz, tu código se rompe.

En Go, esto no sucede. Tu tipo no necesita saber que existe una interfaz. Simplemente, si tienes los métodos correctos, automáticamente satisfaces la interfaz.

type MiTipo struct {
    // campos
}

func (mt MiTipo) Read(p []byte) (int, error) {
    // implementación
}

// MiTipo ahora automáticamente es un Reader
// Pero MiTipo no necesita ni conocer que Reader existe

¿Por qué esto importa? Porque significa desacoplamiento total. Tu código no depende de interfaces que quizás no existían cuando lo escribiste. Tu código es naturalmente flexible.

1.3 La Magia: Satisfacción Implícita

Aquí es donde la mente de muchos desarrolladores se rebela inicialmente. En Go, no necesitas declarar que implementas una interfaz.

Tu código simplemente funciona. Automáticamente.

Veamos un ejemplo concreto:

package main

import (
	"fmt"
	"io"
)

// Logger es una interfaz que define algo que puede escribir logs
type Logger interface {
	Log(message string)
}

// MyLogger implementa Logger, pero sin declararlo explícitamente
type MyLogger struct{}

func (ml MyLogger) Log(message string) {
	fmt.Println("[LOG]", message)
}

// Ahora, una función que acepta un Logger
func ProcessData(logger Logger, data string) {
	logger.Log("Procesando: " + data)
	// más código
}

func main() {
	ml := MyLogger{}

	// Esto funciona. MyLogger satisface Logger automáticamente
	ProcessData(ml, "datos")
}

¿Ves lo que pasó? MyLogger nunca declaró que implementa Logger. Solo tiene un método Log con la firma correcta. Y automáticamente, funciona donde se espera un Logger.

Esto no es un detalle técnico menor. Es fundamental. Significa que puedes:

  1. Escribir un tipo hoy
  2. Alguien escribe una interfaz mañana (que tú no conoces)
  3. Tu tipo automáticamente funciona con esa interfaz
  4. Cero acoplamiento

1.4 Comparación Mental: Java vs Go

Déjame poner lado a lado cómo diferentes lenguajes manejan esto:

Java:

public interface Reader {
    int read(byte[] b) throws IOException;
}

public class FileReader implements Reader {
    // Declara explícitamente que implementa Reader
    public int read(byte[] b) throws IOException {
        // ...
    }
}

// FileReader es un Reader porque lo declaró
// FileReader está acoplado a la interfaz Reader

Python (con typing):

from typing import Protocol

class Reader(Protocol):
    def read(self, b: bytes) -> int:
        ...

class FileReader:
    def read(self, b: bytes) -> int:
        # ...

# FileReader es un Reader porque tiene el método
# Pero el acoplamiento es implícito de otra forma

Go:

type Reader interface {
    Read(p []byte) (int, error)
}

type FileReader struct{}

func (fr FileReader) Read(p []byte) (int, error) {
    // ...
}

// FileReader es un Reader. Automáticamente.
// FileReader no sabe que Reader existe
// Desacoplamiento total

Go logra el desacoplamiento de Python, pero con la seguridad de tipos de Java. Lo mejor de ambos mundos.


Parte 2: Cómo Se Crean Interfaces - Desde Lo Simple Hasta Lo Complejo

2.1 Una Interfaz Mínima

La interfaz más simple posible tiene un método:

type Writer interface {
	Write(p []byte) (n int, err error)
}

Esto dice: “Un Writer es algo que puede escribir bytes.”

Eso es todo.

2.2 Interfaces con Múltiples Métodos

Conforme tu sistema crece, necesitas más comportamiento:

// Un almacén de datos debe poder guardar y recuperar
type DataStore interface {
	Save(key string, value interface{}) error
	Get(key string) (interface{}, error)
	Delete(key string) error
	Exists(key string) bool
}

Esta interfaz define un contrato: “Un DataStore es algo que puede guardar, recuperar, eliminar, y verificar existencia de datos.”

Cualquier tipo que tenga estos cuatro métodos (con estas firmas exactas) automáticamente es un DataStore.

2.3 Interfaces Vacías: interface{}

Go tiene una interfaz especial: interface{}. Es una interfaz que no tiene métodos.

Porque no requiere métodos, todo satisface una interfaz vacía.

var anything interface{} = "string"
anything = 42
anything = true
anything = MyCustomType{}

// Todo funciona. Porque interface{} no requiere nada

¿Para qué sirve? Para cuando necesitas aceptar literalmente cualquier tipo.

En Go 1.18+, interface{} se puede escribir como any (más legible):

var anything any = "string"
// Mismo comportamiento, mejor legibilidad

2.4 Interfaces Embedidas (Composición)

Una de las características más poderosas de Go es que puedes componer interfaces dentro de otras interfaces:

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

// ReadWriter es Reader + Writer
type ReadWriter interface {
	Reader
	Writer
}

// ReadCloser es Reader + Closer
type Closer interface {
	Close() error
}

type ReadCloser interface {
	Reader
	Closer
}

¿Qué significa esto? Si implementas Read y Close, automáticamente implementas ReadCloser.

Esto es composición a nivel de interfaz. No estás heredando código (porque las interfaces no tienen código). Estás componiendo contratos.

Ejemplo real:

type File struct {
	data []byte
	pos  int
}

// Read implementa Reader
func (f *File) Read(p []byte) (int, error) {
	n := copy(p, f.data[f.pos:])
	f.pos += n
	return n, nil
}

// Close implementa Closer
func (f *File) Close() error {
	f.data = nil
	return nil
}

// File automáticamente implementa ReadCloser
// Porque implementa Reader y Closer

func ProcessFile(rc io.ReadCloser) {
	// Puedo leer y cerrar
	rc.Read(make([]byte, 10))
	rc.Close()
}

func main() {
	f := &File{}
	ProcessFile(f) // Funciona. File es un ReadCloser
}

Parte 3: Satisfacción Implícita - La Verdadera Magia

3.1 El Concepto Fundamental

Go NO verifica interfazas en tiempo de compilación de la forma que piensas.

No dice: “Comprueba que este tipo declaró que implementa esta interfaz.”

En su lugar, dice: “¿Tiene este tipo todos los métodos que la interfaz requiere?”

Si la respuesta es sí, es un Reader. Punto. Sin más preguntas.

3.2 Cómo Go Verifica Métodos

Go verifica tres cosas cuando comprueba si un tipo satisface una interfaz:

  1. ¿El nombre del método es exactamente el mismo? (sensible a mayúsculas/minúsculas)
  2. ¿La firma es idéntica? (parámetros y valores de retorno)
  3. ¿El receptor es correcto? (valor o puntero)

Ejemplo - esto funciona:

type Reader interface {
	Read(p []byte) (int, error)
}

type MyReader struct{}

func (mr MyReader) Read(p []byte) (int, error) {
	return 0, nil
}

// MyReader satisface Reader ✓

Esto NO funciona (nombre diferente):

func (mr MyReader) ReadData(p []byte) (int, error) {
	return 0, nil
}

// MyReader NO satisface Reader ✗
// El método se llama ReadData, no Read

Esto NO funciona (firma diferente):

func (mr MyReader) Read(p []byte) int {
	return 0
	// Falta el error en el retorno
}

// MyReader NO satisface Reader ✗
// La firma es diferente

Esto NO funciona (receptor incorrecto):

type Reader interface {
	Read(p []byte) (int, error)
}

type MyReader struct{}

func (mr *MyReader) Read(p []byte) (int, error) {
	// Receptor es puntero
	return 0, nil
}

var r Reader = MyReader{} // ✗ Error
var r Reader = &MyReader{} // ✓ Funciona

Este último punto es importante. Si implementas métodos en un puntero receptor, solo los punteros satisfacen la interfaz.

3.3 La Consecuencia: Desacoplamiento Total

Porque la verificación es puramente por forma (estructura), no por declaración, logras algo imposible en otros lenguajes:

package domain

type Logger interface {
	Log(msg string)
}

type BusinessLogic struct{}

func (bl BusinessLogic) ProcessOrder(logger Logger) {
	logger.Log("Procesando orden")
	// lógica de negocio
}
package adapters

import "myapp/domain"

type ConsoleLogger struct{}

func (cl ConsoleLogger) Log(msg string) {
	fmt.Println(msg)
}

// ConsoleLogger automáticamente satisface domain.Logger
// ConsoleLogger no importa domain.Logger
// Zero acoplamiento

La lógica de negocio define qué logger necesita. El adaptador simplemente tiene el método. Nunca se encuentran en una misma declaración implements. Pero funcionan juntos.

Esto es el desacoplamiento perfecto.


Parte 4: Interfaces Pequeñas vs Grandes - La Regla de Oro

4.1 El Anti-patrón: Interfaces Grandes

En Java, es común ver interfaces enormes:

public interface DataRepository {
    User findUserById(String id);
    List<User> findAllUsers();
    List<User> findUsersByRole(String role);
    List<User> findActiveUsers();
    void saveUser(User user);
    void updateUser(User user);
    void deleteUser(String id);
    int countUsers();
    boolean userExists(String id);
    // ... más métodos
}

¿Por qué? Porque pensamos: “Un repositorio hace todas estas cosas, así que la interfaz debe tener todos estos métodos.”

El problema: tu código que solo necesita leer usuarios debe implementar (o mockear) 9 métodos, aunque solo use 1.

4.2 El Enfoque de Go: Interfaces Pequeñas

Go prefiere interfaces pequeñas. De un método. Incluso.

Las interfaces más importantes de la stdlib de Go tienen 1-3 métodos:

type Reader interface {
	Read(p []byte) (n int, err error)
}

type Writer interface {
	Write(p []byte) (n int, err error)
}

type Closer interface {
	Close() error
}

type Stringer interface {
	String() string
}

Una línea. Un método. Eso es suficiente.

4.3 Por Qué Esto Es Mejor

Interfaces pequeñas son fáciles de satisfacer:

// Cualquier cosa con un String() es un Stringer
type Number int

func (n Number) String() string {
	return fmt.Sprintf("Número: %d", n)
}

// Number es un Stringer. Casi sin esfuerzo.

Interfaces pequeñas son componibles:

// Puedes combinar pequeñas interfaces
type ReadWriter interface {
	Reader
	Writer
}

type ReadWriteCloser interface {
	Reader
	Writer
	Closer
}

// Cada una es útil independientemente
// Pero puedes combinarlas para casos complejos

Interfaces pequeñas son testables:

type Logger interface {
	Log(msg string)
}

// Para testear, creas un mock simple
type TestLogger struct {
	messages []string
}

func (tl *TestLogger) Log(msg string) {
	tl.messages = append(tl.messages, msg)
}

// Un mock para 1 método es trivial

Interfaces pequeñas son honestas:

// Esto dice: "Necesito algo que pueda escribir"
func SaveData(w io.Writer, data []byte) error {
	_, err := w.Write(data)
	return err
}

// Esto dice: "Necesito acceso completo a un repositorio"
func ProcessUsers(repo UserRepository) {
	// Podrías hacer cualquier cosa
}

// Go idiomático usa la primera forma

4.4 Patrón Real: De Interfaces Grandes a Pequeñas

Supongamos que empiezas con esto (mal):

type UserService interface {
	CreateUser(u *User) error
	GetUser(id string) (*User, error)
	UpdateUser(u *User) error
	DeleteUser(id string) error
	ListUsers() ([]*User, error)
	GetUsersByRole(role string) ([]*User, error)
}

El código Go idiomático lo dividiría así:

// Lectura
type UserGetter interface {
	GetUser(id string) (*User, error)
}

type UserLister interface {
	ListUsers() ([]*User, error)
	GetUsersByRole(role string) ([]*User, error)
}

// Escritura
type UserCreator interface {
	CreateUser(u *User) error
}

type UserUpdater interface {
	UpdateUser(u *User) error
}

type UserDeleter interface {
	DeleteUser(id string) error
}

// Ahora, cada función solo depende de lo que necesita
func DisplayUser(getter UserGetter, id string) {
	u, _ := getter.GetUser(id)
	fmt.Println(u)
}

func AdminDeleteUser(deleter UserDeleter, id string) {
	deleter.DeleteUser(id)
}

¿Beneficio? DisplayUser no necesita ni saber que existe UserDeleter. Están completamente desacoplados.


Parte 5: Interfaces en la Práctica - Casos Reales

5.1 Caso Real 1: Parseo de Múltiples Formatos

Imagina que necesitas procesar datos de diferentes formatos (JSON, XML, YAML).

Sin interfaces, escribirías:

func ProcessJSON(data string) {
	// parsear JSON
	// procesar
}

func ProcessXML(data string) {
	// parsear XML
	// procesar
}

func ProcessYAML(data string) {
	// parsear YAML
	// procesar
}

// Código duplicado, difícil de mantener

Con interfaces:

// Define qué necesitas: algo que pueda desserializar
type Unmarshaler interface {
	Unmarshal(data []byte, v interface{}) error
}

// Ahora una sola función funciona con todos
func ProcessData(unmarshaler Unmarshaler, data []byte) (interface{}, error) {
	var result interface{}
	err := unmarshaler.Unmarshal(data, &result)
	return result, err
}

// Diferentes adaptadores
type JSONUnmarshaler struct{}

func (ju JSONUnmarshaler) Unmarshal(data []byte, v interface{}) error {
	return json.Unmarshal(data, v)
}

type XMLUnmarshaler struct{}

func (xu XMLUnmarshaler) Unmarshal(data []byte, v interface{}) error {
	return xml.Unmarshal(data, v)
}

// Uso
func main() {
	jsonData := []byte(`{"key": "value"}`)
	ProcessData(JSONUnmarshaler{}, jsonData)

	xmlData := []byte(`<root><key>value</key></root>`)
	ProcessData(XMLUnmarshaler{}, xmlData)

	// Misma función, diferentes formatos
}

Ventajas:

  • Una sola función procesa múltiples formatos
  • Agregar nuevo formato no requiere cambiar ProcessData
  • Fácil de testear (puedes crear un mock Unmarshaler)

5.2 Caso Real 2: Almacenamiento Flexible

Necesitas guardar datos. Quizás en memoria hoy, base de datos mañana.

// Define el contrato
type Storage interface {
	Set(key string, value interface{}) error
	Get(key string) (interface{}, error)
	Delete(key string) error
	Clear() error
}

// Lógica de negocio solo conoce Storage
type UserCache struct {
	storage Storage
}

func (uc *UserCache) CacheUser(user *User) error {
	return uc.storage.Set(user.ID, user)
}

func (uc *UserCache) GetCachedUser(id string) (*User, error) {
	val, err := uc.storage.Get(id)
	if err != nil {
		return nil, err
	}
	return val.(*User), nil
}

// Implementación en memoria
type MemoryStorage struct {
	data map[string]interface{}
	mu   sync.RWMutex
}

func (ms *MemoryStorage) Set(key string, value interface{}) error {
	ms.mu.Lock()
	defer ms.mu.Unlock()
	ms.data[key] = value
	return nil
}

func (ms *MemoryStorage) Get(key string) (interface{}, error) {
	ms.mu.RLock()
	defer ms.mu.RUnlock()
	val, exists := ms.data[key]
	if !exists {
		return nil, fmt.Errorf("key not found")
	}
	return val, nil
}

// ... más métodos

// Implementación en Redis (mañana)
type RedisStorage struct {
	client *redis.Client
}

func (rs *RedisStorage) Set(key string, value interface{}) error {
	// marshalar y enviar a Redis
}

// ... etc

// Uso - NINGUNO de estos diferencia Storage
func main() {
	var storage Storage = &MemoryStorage{
		data: make(map[string]interface{}),
	}

	cache := &UserCache{storage: storage}
	cache.CacheUser(&User{ID: "1", Name: "Omar"})

	// Luego, cambiar a Redis:
	// var storage Storage = &RedisStorage{client: redisClient}
	// cache := &UserCache{storage: storage}
	// MISMO CÓDIGO DESPUÉS DE UserCache
}

Ventaja principal: Cambiar de almacenamiento no requiere cambiar UserCache. Solo cambias una línea donde creas Storage.

5.3 Caso Real 3: Testing con Interfaces

Las interfaces hacen testing exponencialmente más fácil:

// Tu código de negocio
type OrderProcessor struct {
	db     Database
	logger Logger
	mailer Mailer
}

type Database interface {
	SaveOrder(order *Order) error
	GetOrder(id string) (*Order, error)
}

type Logger interface {
	Log(msg string)
	Error(err error)
}

type Mailer interface {
	SendEmail(to, subject, body string) error
}

func (op *OrderProcessor) ProcessOrder(order *Order) error {
	op.logger.Log("Procesando orden")

	if err := op.db.SaveOrder(order); err != nil {
		op.logger.Error(err)
		return err
	}

	op.logger.Log("Orden guardada")

	if err := op.mailer.SendEmail(order.Email, "Orden confirmada", "..."); err != nil {
		op.logger.Error(err)
		// No falla completamente, solo log del error
	}

	return nil
}

// Ahora, en tus tests, creas mocks simples
type MockDatabase struct {
	SaveOrderCalled bool
	Orders          map[string]*Order
}

func (md *MockDatabase) SaveOrder(order *Order) error {
	md.SaveOrderCalled = true
	md.Orders[order.ID] = order
	return nil
}

func (md *MockDatabase) GetOrder(id string) (*Order, error) {
	order, exists := md.Orders[id]
	if !exists {
		return nil, fmt.Errorf("not found")
	}
	return order, nil
}

type MockLogger struct {
	Messages []string
	Errors   []error
}

func (ml *MockLogger) Log(msg string) {
	ml.Messages = append(ml.Messages, msg)
}

func (ml *MockLogger) Error(err error) {
	ml.Errors = append(ml.Errors, err)
}

type MockMailer struct {
	EmailsSent int
}

func (mm *MockMailer) SendEmail(to, subject, body string) error {
	mm.EmailsSent++
	return nil
}

// Tu test
func TestProcessOrder(t *testing.T) {
	mockDB := &MockDatabase{Orders: make(map[string]*Order)}
	mockLogger := &MockLogger{}
	mockMailer := &MockMailer{}

	processor := &OrderProcessor{
		db:     mockDB,
		logger: mockLogger,
		mailer: mockMailer,
	}

	order := &Order{
		ID:    "123",
		Items: []string{"item1", "item2"},
		Email: "user@example.com",
	}

	err := processor.ProcessOrder(order)

	if err != nil {
		t.Fatalf("ProcessOrder failed: %v", err)
	}

	if !mockDB.SaveOrderCalled {
		t.Error("Database.SaveOrder was not called")
	}

	if mockMailer.EmailsSent != 1 {
		t.Errorf("Expected 1 email, got %d", mockMailer.EmailsSent)
	}

	if len(mockLogger.Messages) == 0 {
		t.Error("Logger was not used")
	}
}

Ventaja: Tu test es rápido (sin bases de datos reales), controlable (puedes simular errores), y claro (ves exactamente qué hace ProcessOrder).

5.4 Caso Real 4: Parámetros Opcionales con Interfaces

Go no tiene “parámetros opcionales” como Python o JavaScript. Pero interfaces te permiten algo equivalente:

// Opción 1: Parámetros varargs (limitado)
func NewServer(addr string, options ...Option) *Server {
	s := &Server{Addr: addr}
	for _, opt := range options {
		opt(s)
	}
	return s
}

type Option func(*Server)

func WithTLS(certFile, keyFile string) Option {
	return func(s *Server) {
		s.TLS = true
		s.CertFile = certFile
		s.KeyFile = keyFile
	}
}

func WithTimeout(timeout time.Duration) Option {
	return func(s *Server) {
		s.Timeout = timeout
	}
}

// Uso
server := NewServer(":8080",
	WithTLS("cert.pem", "key.pem"),
	WithTimeout(30*time.Second),
)

// Opción 2: Builder con interfaz (más flexible)
type ServerBuilder interface {
	WithTLS(certFile, keyFile string) ServerBuilder
	WithTimeout(timeout time.Duration) ServerBuilder
	Build() *Server
}

type serverBuilder struct {
	addr    string
	tls     bool
	certFile string
	keyFile  string
	timeout  time.Duration
}

func NewServerBuilder(addr string) ServerBuilder {
	return &serverBuilder{addr: addr, timeout: 10 * time.Second}
}

func (sb *serverBuilder) WithTLS(certFile, keyFile string) ServerBuilder {
	sb.tls = true
	sb.certFile = certFile
	sb.keyFile = keyFile
	return sb
}

func (sb *serverBuilder) WithTimeout(timeout time.Duration) ServerBuilder {
	sb.timeout = timeout
	return sb
}

func (sb *serverBuilder) Build() *Server {
	return &Server{
		Addr:     sb.addr,
		TLS:      sb.tls,
		CertFile: sb.certFile,
		KeyFile:  sb.keyFile,
		Timeout:  sb.timeout,
	}
}

// Uso
server := NewServerBuilder(":8080").
	WithTLS("cert.pem", "key.pem").
	WithTimeout(30*time.Second).
	Build()

Parte 6: Type Assertions y Type Switches

6.1 Trabajar con interface{}

Cuando almacenas datos en interface{}, a veces necesitas saber qué tipo realmente es.

var data interface{} = "hello"

// Type assertion: intenta convertir a string
str, ok := data.(string)
if ok {
	fmt.Println("Es un string:", str)
} else {
	fmt.Println("No es un string")
}

Si no usas ok, y la aserción falla, panic:

str := data.(string) // ¡PANIC si no es string!

Siempre usa ok para aserciones inseguras.

6.2 Type Switches

Cuando tienes varios tipos posibles, usa type switch:

func Describe(v interface{}) {
	switch v := v.(type) {
	case string:
		fmt.Printf("Es un string: %q\n", v)
	case int:
		fmt.Printf("Es un int: %d\n", v)
	case float64:
		fmt.Printf("Es un float: %f\n", v)
	case []interface{}:
		fmt.Printf("Es un slice: %v\n", v)
	default:
		fmt.Printf("Tipo desconocido: %T\n", v)
	}
}

func main() {
	Describe("hello")       // Es un string: "hello"
	Describe(42)            // Es un int: 42
	Describe(3.14)          // Es un float: 3.140000
	Describe([]interface{}{1, "two"})  // Es un slice: [1 two]
}

Nota importante: v := v.(type) solo funciona en type switches. En otro contexto, sería un error.

6.3 Caso Real: Procesador Flexible de Datos

type DataProcessor struct{}

func (dp *DataProcessor) Process(data interface{}) string {
	switch d := data.(type) {
	case string:
		return strings.ToUpper(d)
	case int:
		return fmt.Sprintf("Número: %d", d*2)
	case float64:
		return fmt.Sprintf("Decimal: %.2f", d*1.5)
	case []interface{}:
		return fmt.Sprintf("Lista de %d elementos", len(d))
	case map[string]interface{}:
		keys := make([]string, 0, len(d))
		for k := range d {
			keys = append(keys, k)
		}
		return fmt.Sprintf("Objeto con keys: %v", keys)
	default:
		return fmt.Sprintf("Tipo desconocido: %T", data)
	}
}

func main() {
	dp := &DataProcessor{}

	fmt.Println(dp.Process("hello"))                                    // HELLO
	fmt.Println(dp.Process(21))                                         // Número: 42
	fmt.Println(dp.Process(2.5))                                        // Decimal: 3.75
	fmt.Println(dp.Process([]interface{}{1, 2, 3}))                     // Lista de 3 elementos
	fmt.Println(dp.Process(map[string]interface{}{"a": 1, "b": 2}))    // Objeto con keys: [a b]
}

Parte 7: Interfaces con Métodos Que Reciben Interfaces

7.1 El Patrón: Métodos que Aceptan Interfaces

Una de las características más poderosas de Go es que los métodos pueden recibir interfaces como parámetros:

type Validator interface {
	Validate() error
}

type Request struct {
	Name  string
	Email string
}

func (r Request) Validate() error {
	if r.Name == "" {
		return fmt.Errorf("Name es requerido")
	}
	if r.Email == "" {
		return fmt.Errorf("Email es requerido")
	}
	return nil
}

// Una función que acepta cualquier cosa que sea validable
func ProcessRequest(v Validator) error {
	if err := v.Validate(); err != nil {
		return fmt.Errorf("Validación fallida: %w", err)
	}

	fmt.Println("Validación exitosa")
	return nil
}

// Request ahora es un Validator
func main() {
	req := Request{Name: "Omar", Email: "omar@example.com"}
	ProcessRequest(req)
}

7.2 Interfaces Que Retornan Interfaces

Los métodos también pueden retornar interfaces:

type DataSource interface {
	ReadData() (io.Reader, error)
}

type FileDataSource struct {
	path string
}

func (fds *FileDataSource) ReadData() (io.Reader, error) {
	file, err := os.Open(fds.path)
	if err != nil {
		return nil, err
	}

	return file, nil // file es un io.Reader
}

type StringDataSource struct {
	data string
}

func (sds *StringDataSource) ReadData() (io.Reader, error) {
	return strings.NewReader(sds.data), nil
}

// Función que procesa cualquier DataSource
func ProcessDataSource(ds DataSource) ([]byte, error) {
	reader, err := ds.ReadData()
	if err != nil {
		return nil, err
	}

	return io.ReadAll(reader)
}

func main() {
	fileSrc := &FileDataSource{path: "data.txt"}
	data, _ := ProcessDataSource(fileSrc)
	fmt.Println(string(data))

	stringSrc := &StringDataSource{data: "hello world"}
	data, _ = ProcessDataSource(stringSrc)
	fmt.Println(string(data))
}

Parte 8: Errores como Interfaces

8.1 Error es una Interfaz

En Go, error es simplemente una interfaz:

type error interface {
	Error() string
}

Eso es todo. Cualquier cosa con un método Error() que retorna string es un error.

type ValidationError struct {
	Field   string
	Message string
}

func (ve ValidationError) Error() string {
	return fmt.Sprintf("Campo '%s': %s", ve.Field, ve.Message)
}

type DatabaseError struct {
	Query string
	Code  int
}

func (de DatabaseError) Error() string {
	return fmt.Sprintf("Error DB (code %d): %s", de.Code, de.Query)
}

func ProcessData(data string) error {
	if data == "" {
		return ValidationError{Field: "data", Message: "no puede estar vacío"}
	}

	if len(data) > 1000 {
		return ValidationError{Field: "data", Message: "es demasiado largo"}
	}

	return nil
}

func main() {
	if err := ProcessData(""); err != nil {
		fmt.Println(err) // Campo 'data': no puede estar vacío
	}
}

8.2 Type Assertions para Manejar Errores Específicos

Porque los errores son interfaces, puedes hacer assertions:

func HandleError(err error) {
	switch e := err.(type) {
	case ValidationError:
		fmt.Printf("Validación fallida en campo '%s': %s\n", e.Field, e.Message)
	case DatabaseError:
		fmt.Printf("Error de BD (código %d)\n", e.Code)
	default:
		fmt.Println("Error desconocido:", err)
	}
}

O verificar si es un tipo específico:

if ve, ok := err.(ValidationError); ok {
	fmt.Printf("Validación fallida en: %s\n", ve.Field)
} else if de, ok := err.(DatabaseError); ok {
	fmt.Printf("Error de BD: código %d\n", de.Code)
}

8.3 errors.Is() y errors.As()

Go 1.13+ te da funciones más robustas para manejar errores:

import "errors"

var NotFoundError = errors.New("no encontrado")

func GetUser(id string) (*User, error) {
	if id == "" {
		return nil, NotFoundError
	}
	// ...
}

func main() {
	_, err := GetUser("")

	// Comparar errores
	if errors.Is(err, NotFoundError) {
		fmt.Println("Usuario no encontrado")
	}
}

Para tipos de error personalizados:

type QueryError struct {
	Query string
}

func (qe QueryError) Error() string {
	return fmt.Sprintf("Error en query: %s", qe.Query)
}

func runQuery() error {
	return QueryError{Query: "SELECT * FROM users"}
}

func main() {
	err := runQuery()

	// errors.As desenvuelve la cadena de errores
	var qe QueryError
	if errors.As(err, &qe) {
		fmt.Println("Query que falló:", qe.Query)
	}
}

Parte 9: Patrones Avanzados

9.1 Wrapper de Interfaces

Puedes “envolver” una interfaz con comportamiento adicional:

type Logger interface {
	Log(msg string)
}

type LoggerWithMetrics struct {
	logger Logger
	count  int
}

func (lwm *LoggerWithMetrics) Log(msg string) {
	lwm.count++
	fmt.Printf("[%d] ", lwm.count)
	lwm.logger.Log(msg)
}

type ConsoleLogger struct{}

func (cl ConsoleLogger) Log(msg string) {
	fmt.Println(msg)
}

func main() {
	console := ConsoleLogger{}
	withMetrics := &LoggerWithMetrics{logger: console}

	withMetrics.Log("Mensaje 1")
	withMetrics.Log("Mensaje 2")

	// Output:
	// [1] Mensaje 1
	// [2] Mensaje 2
}

9.2 Cadena de Responsabilidad con Interfaces

type Handler interface {
	Handle(request *Request) Response
	SetNext(handler Handler)
}

type BaseHandler struct {
	next Handler
}

func (bh *BaseHandler) SetNext(handler Handler) {
	bh.next = handler
}

type AuthHandler struct {
	BaseHandler
}

func (ah *AuthHandler) Handle(request *Request) Response {
	if request.Token == "" {
		return Response{Status: 401, Body: "Token requerido"}
	}

	if ah.next != nil {
		return ah.next.Handle(request)
	}

	return Response{Status: 200, Body: "OK"}
}

type RoleHandler struct {
	BaseHandler
	requiredRole string
}

func (rh *RoleHandler) Handle(request *Request) Response {
	if request.Role != rh.requiredRole {
		return Response{Status: 403, Body: "Rol insuficiente"}
	}

	if rh.next != nil {
		return rh.next.Handle(request)
	}

	return Response{Status: 200, Body: "OK"}
}

func main() {
	auth := &AuthHandler{}
	role := &RoleHandler{requiredRole: "admin"}

	auth.SetNext(role)

	request := &Request{Token: "valid", Role: "admin"}
	response := auth.Handle(request)

	fmt.Println(response)
}

9.3 Middleware Pattern (Decorador)

type Handler func(http.ResponseWriter, *http.Request)

func LoggingMiddleware(next Handler) Handler {
	return func(w http.ResponseWriter, r *http.Request) {
		fmt.Printf("Recibido: %s %s\n", r.Method, r.RequestURI)
		next(w, r)
		fmt.Println("Respuesta enviada")
	}
}

func AuthMiddleware(next Handler) Handler {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Header.Get("Authorization") == "" {
			w.WriteHeader(http.StatusUnauthorized)
			return
		}
		next(w, r)
	}
}

func MyHandler(w http.ResponseWriter, r *http.Request) {
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("Hola"))
}

func main() {
	// Aplicar middleware en orden
	handler := LoggingMiddleware(AuthMiddleware(MyHandler))

	http.HandleFunc("/", handler)
	http.ListenAndServe(":8080", nil)
}

Parte 10: Errores Comunes y Cómo Evitarlos

10.1 Olvidar el Receptor Correcto

type Reader interface {
	Read(p []byte) (int, error)
}

type MyReader struct{}

// ❌ INCORRECTO: receptor valor, pero la interfaz espera puntero
func (mr MyReader) Read(p []byte) (int, error) {
	return 0, nil
}

var r Reader = MyReader{} // ❌ Error: MyReader no satisface Reader

// ✅ CORRECTO: receptor puntero
func (mr *MyReader) Read(p []byte) (int, error) {
	return 0, nil
}

var r Reader = &MyReader{} // ✅ Funciona

Regla: Si implementas métodos con receptor puntero, solo los punteros satisfacen la interfaz.

10.2 Typos en Nombres de Métodos

type Writer interface {
	Write(p []byte) (int, error)
}

type MyWriter struct{}

// ❌ INCORRECTO: se llama "WriteData" no "Write"
func (mw MyWriter) WriteData(p []byte) (int, error) {
	return len(p), nil
}

var w Writer = MyWriter{} // ❌ Error: nombre incorrecto

// ✅ CORRECTO
func (mw MyWriter) Write(p []byte) (int, error) {
	return len(p), nil
}

var w Writer = MyWriter{} // ✅ Funciona

Go es sensible a mayúsculas/minúsculas. Write no es lo mismo que write.

10.3 Firmas Diferentes

type Reader interface {
	Read(p []byte) (int, error)
}

type MyReader struct{}

// ❌ INCORRECTO: falta el error en el retorno
func (mr MyReader) Read(p []byte) int {
	return len(p)
}

var r Reader = MyReader{} // ❌ Error: firma diferente

// ✅ CORRECTO
func (mr MyReader) Read(p []byte) (int, error) {
	return len(p), nil
}

var r Reader = MyReader{} // ✅ Funciona

La firma debe ser idéntica. Los tipos de parámetros, el orden, y los tipos de retorno deben coincidir exactamente.

10.4 Asumir que nil es Válido en Interfaces

var r io.Reader
// r es nil

data := make([]byte, 10)
n, err := r.Read(data) // ❌ PANIC: nil pointer dereference

// ✅ CORRECTO: verifica si es nil
var r io.Reader
if r != nil {
	r.Read(data)
}

Siempre verifica que una interfaz no sea nil antes de usarla.

10.5 Confundir Punteros con Valores

type Logger interface {
	Log(msg string)
}

type MyLogger struct {
	count int
}

func (ml *MyLogger) Log(msg string) {
	ml.count++
	fmt.Println(msg)
}

func main() {
	// ❌ Esto funciona, pero es incorrecto
	var ml MyLogger
	var l Logger = ml // Compila porque se puede convertir valor a puntero si el receptor es puntero

	// Pero no es recomendado. ✅ Mejor:
	ml := &MyLogger{}
	var l Logger = ml
}

Conclusión: Por Qué Las Interfaces de Go Cambiarán Tu Forma de Programar

Después de explorar todo esto, puedes ver por qué Go diseñó las interfaces de esta manera:

  1. Desacoplamiento: Tu código no depende de interfaces que quizás no sabía que existían
  2. Simplicidad: Las interfaces son lo más simple posible mientras son poderosas
  3. Composabilidad: Las interfaces pequeñas se componen en interfaces más grandes
  4. Flexibilidad: Puedes cambiar implementaciones sin tocar código que usa interfaces
  5. Testabilidad: Los mocks son triviales de crear
  6. Claridad: El código es más explícito sobre qué necesita

Go te fuerza a pensar diferente. No piensas en jerarquías. No piensas en relaciones de clase. Piensas en comportamientos, en contratos, en qué hace una cosa, no en qué es una cosa.

Y cuando finalmente haces ese clic mental, cuando entienes que interfaces en Go son sobre desacoplamiento, flexibilidad y simplicidad, tu forma de diseñar software cambia para siempre.

Escribes código que:

  • Es fácil de entender
  • Es fácil de testear
  • Es fácil de cambiar
  • Es fácil de extender
  • Escala con tu negocio

Eso es la verdadera magia de las interfaces en Go.

Tags

#golang #interfaces #design-patterns #architecture #go-1.25 #software-design