A logo showing the text blog.marcnuri.com
English
Inicio»Desarrollo Backend»Interfaces en Go: Patrones de Diseño y Buenas Prácticas

Entradas Recientes

  • Fabric8 Kubernetes Client 7.4.0 está disponible!
  • Kubernetes MCP Server se une a la organización Containers
  • MCP Tool Annotations: Añadiendo Metadatos y Contexto a Tus Herramientas de IA
  • Fabric8 Kubernetes Client 7.2.0 está disponible!
  • Conectarse a un servidor MCP con JavaScript y AI SDK

Categorías

  • Antiguo
  • Cloud Native
  • Desarrollo Backend
  • Desarrollo Frontend
  • Herramientas
  • Ingeniería de calidad
  • Inteligencia Artificial
  • JavaScript
  • Operaciones
  • Personal
  • Proyectos personales
  • Reflexiones sobre Ingeniería

Archivos

  • agosto 2025
  • junio 2025
  • abril 2025
  • marzo 2025
  • febrero 2025
  • enero 2025
  • diciembre 2025
  • noviembre 2024
  • octubre 2024
  • julio 2024
  • mayo 2024
  • abril 2024
  • marzo 2024
  • febrero 2024
  • enero 2024
  • diciembre 2024
  • noviembre 2023
  • octubre 2023
  • septiembre 2023
  • agosto 2023
  • julio 2023
  • junio 2023
  • mayo 2023
  • abril 2023
  • marzo 2023
  • febrero 2023
  • enero 2023
  • diciembre 2023
  • noviembre 2022
  • octubre 2022
  • septiembre 2022
  • agosto 2022
  • julio 2022
  • junio 2022
  • mayo 2022
  • abril 2022
  • febrero 2022
  • enero 2022
  • diciembre 2022
  • noviembre 2021
  • octubre 2021
  • septiembre 2021
  • agosto 2021
  • julio 2021
  • junio 2021
  • diciembre 2021
  • noviembre 2020
  • septiembre 2020
  • agosto 2020
  • julio 2020
  • mayo 2020
  • abril 2020
  • febrero 2020
  • enero 2020
  • diciembre 2020
  • octubre 2019
  • agosto 2019
  • junio 2019
  • noviembre 2018
  • julio 2018
  • junio 2018
  • mayo 2018
  • abril 2018
  • febrero 2018
  • enero 2018
  • octubre 2017
  • septiembre 2017
  • julio 2017
  • junio 2017
  • diciembre 2017
  • junio 2016
  • diciembre 2016
  • noviembre 2015
  • octubre 2015
  • noviembre 2014
  • febrero 2014
  • enero 2011
  • mayo 2008
  • abril 2008
  • marzo 2008
  • diciembre 2008
  • mayo 2007
  • abril 2007
  • marzo 2007
  • febrero 2007

Interfaces en Go: Patrones de Diseño y Buenas Prácticas

2020-03-14 en Desarrollo Backend etiquetado Go / Buenas Prácticas por Marc Nuri | Última actualización: 2025-09-24
English version

Introducción

Las interfaces en Go representan una de las características más elegantes y potentes del lenguaje, fundamentalmente diferentes de las interfaces en lenguajes orientados a objetos tradicionales. A diferencia de la implementación explícita de interfaces que encontramos en Java o C#, la satisfacción implícita de interfaces de Go crea un sistema flexible que promueve código limpio y desacoplado sin la ceremonia de las jerarquías de herencia.

La simplicidad de las interfaces en Go es su principal fortaleza: cualquier tipo que implemente los métodos requeridos satisface automáticamente la interfaz. Esto fomenta la composición sobre la herencia y ayuda a hacer tu código modular, testeable y adaptable. Pero con esta flexibilidad viene responsabilidad: interfaces mal diseñadas llevan a acoplamiento, confusión y APIs frágiles.

En este artículo, te mostraré los patrones de diseño y buenas prácticas que te ayudarán a aprovechar las interfaces de Go a su máximo potencial. Ya estés construyendo microservicios, herramientas CLI o sistemas distribuidos complejos, entender estos patrones hará tu código Go más robusto y elegante.

Entendiendo los Fundamentos de las Interfaces de Go

Antes de adentrarnos en los patrones de diseño, establezcamos una base sólida sobre cómo funcionan las interfaces en Go y qué las hace especiales.

El Sistema de Interfaces Implícitas

El modelo de interfaces de Go utiliza tipado estructural: un tipo no necesita declarar explícitamente que "implementa" una interfaz. Si tiene las firmas de métodos necesarias, satisface la interfaz.

implicit_interface.go
package main

import "fmt"

// Writer interface define un contrato para escribir
type Writer interface {
	Write(data []byte) (int, error)
}

// FileWriter implementa Writer implícitamente
type FileWriter struct {
	filename string
}

func (fw *FileWriter) Write(data []byte) (int, error) {
	fmt.Printf("Writing %d bytes to file: %s\n", len(data), fw.filename)
	return len(data), nil
}

// NetworkWriter también implementa Writer implícitamente
type NetworkWriter struct {
	endpoint string
}

func (nw *NetworkWriter) Write(data []byte) (int, error) {
	fmt.Printf("Sending %d bytes to endpoint: %s\n", len(data), nw.endpoint)
	return len(data), nil
}

func processData(w Writer, data []byte) error {
	_, err := w.Write(data)
	return err
}

func main() {
	data := []byte("Hello, Go interfaces!")

	fileWriter := &FileWriter{filename: "output.txt"}
	networkWriter := &NetworkWriter{endpoint: "api.example.com"}

	processData(fileWriter, data)
	processData(networkWriter, data)
}

En este ejemplo, processData define la interfaz que espera. Tipos como FileWriter y NetworkWriter la satisfacen automáticamente implementando el método Write, sin ninguna declaración explícita. Este patrón de definir interfaces en el lado del consumidor fomenta el bajo acoplamiento.

Valores de Interfaz y Dynamic Dispatch

Internamente, los valores de interfaz de Go contienen dos componentes: un tipo y un valor. Esto permite el despacho dinámico manteniendo la seguridad de tipos:

interface_values.go
package main

import (
	"fmt"
	"reflect"
)

type Greeter interface {
	Greet() string
}

type EnglishGreeter struct {
	name string
}

func (eg EnglishGreeter) Greet() string {
	return fmt.Sprintf("Hello, %s!", eg.name)
}

type SpanishGreeter struct {
	name string
}

func (sg SpanishGreeter) Greet() string {
	return fmt.Sprintf("¡Hola, %s!", sg.name)
}

func main() {
	var greeter Greeter

	// El valor de la interface es nil inicialmente
	fmt.Printf("Initial: %v, %T\n", greeter, greeter)

	// Asignar valor EnglishGreeter
	greeter = EnglishGreeter{name: "Aitana"}
	fmt.Printf("English: %s, Type: %s\n", greeter.Greet(), reflect.TypeOf(greeter))

	// Asignar valor SpanishGreeter
	greeter = SpanishGreeter{name: "Àlex"}
	fmt.Printf("Spanish: %s, Type: %s\n", greeter.Greet(), reflect.TypeOf(greeter))
}

En este ejemplo, la variable greeter de tipo Greeter puede contener valores de diferentes tipos concretos (EnglishGreeter y SpanishGreeter). Cuando se asigna un valor a greeter, el valor de la interfaz contiene tanto el tipo concreto como el valor, permitiendo que las llamadas a métodos se despachen a la implementación correcta en tiempo de ejecución.

Entender esta naturaleza dual de los valores de interfaz es crucial para el diseño efectivo de interfaces y ayuda a evitar errores comunes como problemas de comparación de interfaces nil.

Principales Patrones de Diseño de Interfaces

Ahora aprendamos sobre algunos de los patrones fundamentales que forman los bloques de construcción del diseño efectivo de interfaces en Go.

1. El Patrón Strategy

El patrón Strategy te permite definir una familia de algoritmos, encapsular cada uno y hacerlos intercambiables. Las interfaces de Go hacen este patrón particularmente elegante:

strategy_pattern.go
package main

import (
	"fmt"
	"sort"
)

// SortStrategy define diferentes enfoques de ordenamiento
type SortStrategy interface {
	Sort(data []string) []string
}

// AlphabeticalSort implementa ordenamiento lexicográfico
type AlphabeticalSort struct{}

func (as AlphabeticalSort) Sort(data []string) []string {
	result := make([]string, len(data))
	copy(result, data)
	sort.Strings(result)
	return result
}

// LengthSort ordena por longitud de cadena
type LengthSort struct{}

func (ls LengthSort) Sort(data []string) []string {
	result := make([]string, len(data))
	copy(result, data)
	sort.Slice(result, func(i, j int) bool {
		return len(result[i]) < len(result[j])
	})
	return result
}

// ReverseSort ordena en orden alfabético inverso
type ReverseSort struct{}

func (rs ReverseSort) Sort(data []string) []string {
	result := make([]string, len(data))
	copy(result, data)
	sort.Sort(sort.Reverse(sort.StringSlice(result)))
	return result
}

// DataProcessor usa una estrategia para procesar datos
type DataProcessor struct {
	strategy SortStrategy
}

func (dp *DataProcessor) SetStrategy(strategy SortStrategy) {
	dp.strategy = strategy
}

func (dp *DataProcessor) ProcessData(data []string) []string {
	if dp.strategy == nil {
		return data
	}
	return dp.strategy.Sort(data)
}

func main() {
	data := []string{"banana", "apple", "cherry", "date", "elderberry"}
	processor := &DataProcessor{}

	// Usar diferentes estrategias
	strategies := map[string]SortStrategy{
		"Alphabetical": AlphabeticalSort{},
		"By Length":    LengthSort{},
		"Reverse":      ReverseSort{},
	}

	for name, strategy := range strategies {
		processor.SetStrategy(strategy)
		result := processor.ProcessData(data)
		fmt.Printf("%s: %v\n", name, result)
	}
}

Este patrón es particularmente útil en aplicaciones dirigidas por configuración donde el comportamiento necesita cambiar basado en preferencias del usuario o configuraciones del entorno. Cuando la aplicación se inicia, una estrategia específica puede ser seleccionada basada en la configuración, permitiendo código flexible y mantenible.

2. El Patrón Decorator

El patrón decorator te permite añadir comportamiento a objetos dinámicamente sin alterar su estructura. La composición de interfaces de Go hace que este patrón sea natural:

decorator_pattern.go
package main

import (
	"fmt"
	"strings"
	"time"
)

// MessageProcessor define la interface central
type MessageProcessor interface {
	Process(message string) string
}

// BaseProcessor proporciona procesamiento básico de mensajes
type BaseProcessor struct{}

func (bp BaseProcessor) Process(message string) string {
	return message
}

// UppercaseDecorator convierte mensajes a mayúsculas
type UppercaseDecorator struct {
	processor MessageProcessor
}

func (ud UppercaseDecorator) Process(message string) string {
	result := ud.processor.Process(message)
	return strings.ToUpper(result)
}

// TimestampDecorator añade marcas de tiempo a los mensajes
type TimestampDecorator struct {
	processor MessageProcessor
}

func (td TimestampDecorator) Process(message string) string {
	result := td.processor.Process(message)
	timestamp := time.Now().Format("2006-01-02 15:04:05")
	return fmt.Sprintf("[%s] %s", timestamp, result)
}

// PrefixDecorator añade un prefijo personalizado
type PrefixDecorator struct {
	processor MessageProcessor
	prefix    string
}

func (pd PrefixDecorator) Process(message string) string {
	result := pd.processor.Process(message)
	return fmt.Sprintf("%s: %s", pd.prefix, result)
}

func main() {
	// Construir una cadena de decoradores
	base := BaseProcessor{}

	// Añadir decorador de timestamp
	withTimestamp := TimestampDecorator{processor: base}

	// Añadir decorador de mayúsculas
	withUppercase := UppercaseDecorator{processor: withTimestamp}

	// Añadir decorador de prefijo
	withPrefix := PrefixDecorator{
		processor: withUppercase,
		prefix:    "LOG",
	}

	message := "User authentication successful to blog.marcnuri.com"
	result := withPrefix.Process(message)
	fmt.Println(result)

	// Demostrar diferentes combinaciones
	simpleChain := UppercaseDecorator{processor: base}
	fmt.Println(simpleChain.Process("Simple message"))
}

Este patrón funciona muy bien en sistemas de middleware, frameworks de logging y en cualquier lugar donde necesites añadir funcionalidades transversales sin modificar la lógica de negocio central.

La salida del ejemplo anterior se verá así:

LOG: [2015-10-21 04:29:00] USER AUTHENTICATION SUCCESSFUL TO BLOG.MARCNURI.COM
SIMPLE MESSAGE

Puedes ver cómo los decoradores pueden apilarse para crear pipelines de procesamiento complejos manteniendo cada componente enfocado y reutilizable.

3. El Patrón Observer

El patrón observer define una dependencia uno-a-muchos entre objetos, permitiendo que múltiples observadores sean notificados de cambios de estado:

observer_pattern.go
package main

import (
	"fmt"
	"sync"
	"time"
)

// Observer define la interface para objetos que deberían ser notificados
type Observer interface {
	Update(event Event)
}

// Event representa una notificación con datos
type Event struct {
	Type string
	Data interface{}
}

// Subject gestiona observadores y notificaciones
type Subject interface {
	Subscribe(observer Observer)
	Unsubscribe(observer Observer)
	Notify(event Event)
}

// EventBus implementa un subject thread-safe
type EventBus struct {
	observers []Observer
	mutex     sync.RWMutex
}

func (eb *EventBus) Subscribe(observer Observer) {
	eb.mutex.Lock()
	defer eb.mutex.Unlock()
	eb.observers = append(eb.observers, observer)
}

func (eb *EventBus) Unsubscribe(observer Observer) {
	eb.mutex.Lock()
	defer eb.mutex.Unlock()
	for i, obs := range eb.observers {
		if obs == observer {
			eb.observers = append(eb.observers[:i], eb.observers[i+1:]...)
			break
		}
	}
}

func (eb *EventBus) Notify(event Event) {
	eb.mutex.RLock()
	defer eb.mutex.RUnlock()
	for _, observer := range eb.observers {
		go observer.Update(event) // Notificación asíncrona
	}
}

// Observadores concretos
type EmailNotifier struct {
	email string
}

func (en EmailNotifier) Update(event Event) {
	fmt.Printf("Email to %s: %s event - %v\n", en.email, event.Type, event.Data)
}

type SMSNotifier struct {
	phone string
}

func (sn SMSNotifier) Update(event Event) {
	fmt.Printf("SMS to %s: %s event - %v\n", sn.phone, event.Type, event.Data)
}

type LogNotifier struct{}

func (ln LogNotifier) Update(event Event) {
	fmt.Printf("LOG: %s event occurred with data: %v\n", event.Type, event.Data)
}

// UserService demuestra el patrón en acción
type UserService struct {
	eventBus Subject
}

func (us *UserService) CreateUser(username string) {
	// Simular creación de usuario
	fmt.Printf("Creating user: %s\n", username)

	// Notificar observadores
	us.eventBus.Notify(Event{
		Type: "USER_CREATED",
		Data: map[string]string{"username": username},
	})
}

func main() {
	eventBus := &EventBus{}
	userService := &UserService{eventBus: eventBus}

	// Suscribir diferentes tipos de observadores
	emailNotifier := EmailNotifier{email: "admin@example.com"}
	smsNotifier := SMSNotifier{phone: "+1234567890"}
	logNotifier := LogNotifier{}

	eventBus.Subscribe(emailNotifier)
	eventBus.Subscribe(smsNotifier)
	eventBus.Subscribe(logNotifier)

	// Disparar un evento
	userService.CreateUser("aitana")

	// Dar tiempo a las notificaciones asíncronas para completarse
	time.Sleep(100 * time.Millisecond)
}

Este patrón es esencial para arquitecturas event-driven, notificaciones en tiempo real y componentes de sistema desacoplados.

Patrones Avanzados de Composición de Interfaces

Las capacidades de embebido y composición de interfaces de Go permiten patrones de diseño sofisticados que promueven la reutilización de código y la mantenibilidad.

Segregación y Composición de Interfaces

El Principio de Segregación de Interfaces sugiere que los clientes no deberían ser forzados a depender de interfaces que no usan. Go hace esto natural a través de la composición de interfaces:

interface_segregation.go
package main

import "fmt"

// Interfaces pequeñas y enfocadas

type Printer interface {
	Print(doc string)
}

type Scanner interface {
	Scan() string
}


// Interfaces compuestas para casos de uso específicos

type MultiFunctionDevice interface {
	Printer
	Scanner
}

// Tipos concretos

type SimplePrinter struct{}

func (p SimplePrinter) Print(doc string) {
	fmt.Println("Printing:", doc)
}

type MultiFunctionMachine struct{}

func (m MultiFunctionMachine) Print(doc string) {
	fmt.Println("Printing:", doc)
}

func (m MultiFunctionMachine) Scan() string {
	return "Scanned document"
}

func main() {
	var printer Printer = SimplePrinter{}
	printer.Print("Hello, ISP!")

	var mfm MultiFunctionDevice = MultiFunctionMachine{}
	mfm.Print("Report.pdf")
	fmt.Println(mfm.Scan())
}

En este ejemplo, Printer y Scanner son interfaces pequeñas y enfocadas. La interfaz MultiFunctionDevice compone ambas, permitiendo a los clientes depender solo de la funcionalidad que necesitan.

El tipo SimplePrinter solo implementa la interfaz Printer, mientras que MultiFunctionMachine implementa ambas. En la función main, demostramos cómo diferentes clientes pueden usar estas interfaces sin ser forzados a depender de métodos innecesarios.

Este enfoque crea APIs flexibles donde los clientes dependen solo de la funcionalidad que realmente necesitan, haciendo el código más mantenible y testeable.

El Patrón Adapter con Interfaces

El patrón adapter permite que interfaces incompatibles trabajen juntas. Las interfaces de Go hacen este patrón particularmente limpio:

adapter_pattern.go
package main

import (
	"fmt"
	"log"
	"os"
	"strings"
)

// Interface moderna que nuestra aplicación espera
type Logger interface {
	Info(message string)
	Error(message string)
	Debug(message string)
}

// Sistema de logging legacy que necesitamos integrar
type LegacyLogger struct {
	prefix string
}

func (ll *LegacyLogger) LogMessage(level, message string) {
	fmt.Printf("[%s] %s: %s\n", ll.prefix, strings.ToUpper(level), message)
}

// Adapter para hacer que LegacyLogger implemente Logger
type LegacyLoggerAdapter struct {
	legacy *LegacyLogger
}

func (lla *LegacyLoggerAdapter) Info(message string) {
	lla.legacy.LogMessage("info", message)
}

func (lla *LegacyLoggerAdapter) Error(message string) {
	lla.legacy.LogMessage("error", message)
}

func (lla *LegacyLoggerAdapter) Debug(message string) {
	lla.legacy.LogMessage("debug", message)
}

// Librería de terceros con interface diferente
type ThirdPartyLogger struct{}

func (tpl *ThirdPartyLogger) WriteLog(severity int, msg string) {
	severityMap := map[int]string{
		1: "DEBUG",
		2: "INFO",
		3: "ERROR",
	}
	fmt.Printf("[THIRD-PARTY] %s: %s\n", severityMap[severity], msg)
}

// Adapter para logger de terceros
type ThirdPartyLoggerAdapter struct {
	thirdParty *ThirdPartyLogger
}

func (tpla *ThirdPartyLoggerAdapter) Info(message string) {
	tpla.thirdParty.WriteLog(2, message)
}

func (tpla *ThirdPartyLoggerAdapter) Error(message string) {
	tpla.thirdParty.WriteLog(3, message)
}

func (tpla *ThirdPartyLoggerAdapter) Debug(message string) {
	tpla.thirdParty.WriteLog(1, message)
}

// Adapter de la librería estándar
type StdLoggerAdapter struct {
	logger *log.Logger
}

func (sla *StdLoggerAdapter) Info(message string) {
	sla.logger.Printf("INFO: %s", message)
}

func (sla *StdLoggerAdapter) Error(message string) {
	sla.logger.Printf("ERROR: %s", message)
}

func (sla *StdLoggerAdapter) Debug(message string) {
	sla.logger.Printf("DEBUG: %s", message)
}

// Servicio de aplicación que usa la interface Logger
type UserService struct {
	logger Logger
}

func (us *UserService) CreateUser(username string) error {
	us.logger.Info(fmt.Sprintf("Creating user: %s", username))

	// Simular algo de procesamiento
	if username == "" {
		us.logger.Error("Username cannot be empty")
		return fmt.Errorf("invalid username")
	}

	us.logger.Debug(fmt.Sprintf("User %s created successfully", username))
	return nil
}

func main() {
	// Usar diferentes implementaciones de logger a través de adapters
	loggers := map[string]Logger{
		"Legacy": &LegacyLoggerAdapter{
			legacy: &LegacyLogger{prefix: "LEGACY"},
		},
		"ThirdParty": &ThirdPartyLoggerAdapter{
			thirdParty: &ThirdPartyLogger{},
		},
		"Standard": &StdLoggerAdapter{
			logger: log.New(os.Stdout, "STD ", log.LstdFlags),
		},
	}

	for name, logger := range loggers {
		fmt.Printf("\n--- Using %s Logger ---\n", name)
		service := &UserService{logger: logger}
		service.CreateUser("julia")
		service.CreateUser("") // Esto causará un error
	}
}

En este ejemplo, tenemos una interfaz Logger que nuestra aplicación espera. Luego creamos adapters para un sistema de logging legacy, un logger de terceros y el logger de la librería estándar para conformar con esta interfaz. Cada adapter traduce las llamadas de nuestra aplicación a los métodos apropiados del sistema de logging subyacente.

Como puedes ver, el patrón adapter es particularmente valioso cuando se integra con librerías externas, sistemas legacy o cuando necesitas estandarizar interfaces a través de diferentes implementaciones.

Estrategias de Testing y Mocking con Interfaces

El testing es donde las interfaces de Go realmente brillan. Permiten mocking fácil e inyección de dependencias, haciendo las pruebas unitarias aisladas y confiables.

Aquí hay un ejemplo completo de cómo estructurar tu código de producción para testabilidad usando interfaces:

interface_based_testing.go
package main

import (
	"fmt"
	"time"
)

// Dependencias externas como interfaces
type TimeProvider interface {
	Now() time.Time
}

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

type UserStore interface {
	SaveUser(user *User) error
	GetUser(email string) (*User, error)
}

// Modelo de dominio
type User struct {
	Email       string
	Password    string
	IsVerified  bool
	CreatedAt   time.Time
	VerifiedAt  *time.Time
}

// Servicio con dependencias inyectadas como interfaces
type UserRegistrationService struct {
	timeProvider TimeProvider
	emailSender  EmailSender
	userStore    UserStore
}

func NewUserRegistrationService(
	timeProvider TimeProvider,
	emailSender EmailSender,
	userStore UserStore,
) *UserRegistrationService {
	return &UserRegistrationService{
		timeProvider: timeProvider,
		emailSender:  emailSender,
		userStore:    userStore,
	}
}

func (s *UserRegistrationService) RegisterUser(email, password string) error {
	// Verificar si el usuario ya existe
	existing, _ := s.userStore.GetUser(email)
	if existing != nil {
		return fmt.Errorf("user already exists")
	}

	// Crear usuario
	user := &User{
		Email:      email,
		Password:   password, // En código real, esto estaría hasheado
		IsVerified: false,
		CreatedAt:  s.timeProvider.Now(),
	}

	// Guardar usuario
	if err := s.userStore.SaveUser(user); err != nil {
		return fmt.Errorf("failed to save user: %w", err)
	}

	// Enviar email de verificación
	subject := "Verify your account"
	body := fmt.Sprintf("Please verify your account by clicking the link...")
	if err := s.emailSender.SendEmail(email, subject, body); err != nil {
		return fmt.Errorf("failed to send verification email: %w", err)
	}

	return nil
}

func (s *UserRegistrationService) VerifyUser(email string) error {
	user, err := s.userStore.GetUser(email)
	if err != nil {
		return fmt.Errorf("user not found: %w", err)
	}

	if user.IsVerified {
		return fmt.Errorf("user already verified")
	}

	now := s.timeProvider.Now()
	user.IsVerified = true
	user.VerifiedAt = &now

	return s.userStore.SaveUser(user)
}

// Implementaciones reales
type RealTimeProvider struct{}

func (r RealTimeProvider) Now() time.Time {
	return time.Now()
}

type RealEmailSender struct{}

func (r RealEmailSender) SendEmail(to, subject, body string) error {
	fmt.Printf("Sending email to %s: %s\n", to, subject)
	return nil
}

type RealUserStore struct {
	users map[string]*User
}

func NewRealUserStore() *RealUserStore {
	return &RealUserStore{users: make(map[string]*User)}
}

func (r *RealUserStore) SaveUser(user *User) error {
	r.users[user.Email] = user
	return nil
}

func (r *RealUserStore) GetUser(email string) (*User, error) {
	user, exists := r.users[email]
	if !exists {
		return nil, fmt.Errorf("user not found")
	}
	return user, nil
}

func main() {
	// Ejemplo de uso con implementaciones reales
	timeProvider := RealTimeProvider{}
	emailSender := RealEmailSender{}
	userStore := NewRealUserStore()

	service := NewUserRegistrationService(timeProvider, emailSender, userStore)

	err := service.RegisterUser("alice@example.com", "password123")
	if err != nil {
		fmt.Printf("Registration failed: %v\n", err)
		return
	}

	fmt.Println("User registered successfully!")

	err = service.VerifyUser("alice@example.com")
	if err != nil {
		fmt.Printf("Verification failed: %v\n", err)
		return
	}

	fmt.Println("User verified successfully!")
}

Ahora veamos cómo escribir tests para este servicio usando implementaciones mock de las interfaces:

interface_based_testing_test.go
package main

import (
	"fmt"
	"testing"
	"time"
)

// Mocks de test
type MockTimeProvider struct {
	currentTime time.Time
}

func (m *MockTimeProvider) Now() time.Time {
	return m.currentTime
}

type MockEmailSender struct {
	sentEmails []EmailRecord
	shouldFail bool
}

type EmailRecord struct {
	To      string
	Subject string
	Body    string
}

func (m *MockEmailSender) SendEmail(to, subject, body string) error {
	if m.shouldFail {
		return fmt.Errorf("email sending failed")
	}

	m.sentEmails = append(m.sentEmails, EmailRecord{
		To:      to,
		Subject: subject,
		Body:    body,
	})
	return nil
}

type MockUserStore struct {
	users      map[string]*User
	shouldFail bool
}

func NewMockUserStore() *MockUserStore {
	return &MockUserStore{users: make(map[string]*User)}
}

func (m *MockUserStore) SaveUser(user *User) error {
	if m.shouldFail {
		return fmt.Errorf("database error")
	}
	m.users[user.Email] = user
	return nil
}

func (m *MockUserStore) GetUser(email string) (*User, error) {
	if m.shouldFail {
		return nil, fmt.Errorf("database error")
	}

	user, exists := m.users[email]
	if !exists {
		return nil, fmt.Errorf("user not found")
	}
	return user, nil
}

func TestUserRegistration(t *testing.T) {
	// Configurar mocks
	fixedTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &MockTimeProvider{currentTime: fixedTime}
	emailSender := &MockEmailSender{}
	userStore := NewMockUserStore()

	service := NewUserRegistrationService(timeProvider, emailSender, userStore)

	// Test de registro exitoso
	err := service.RegisterUser("test@example.com", "password123")
	if err != nil {
		t.Fatalf("Expected no error, got: %v", err)
	}

	// Verificar que el usuario fue guardado
	savedUser, err := userStore.GetUser("test@example.com")
	if err != nil {
		t.Fatalf("User should be saved: %v", err)
	}

	if savedUser.Email != "test@example.com" {
		t.Errorf("Expected email test@example.com, got %s", savedUser.Email)
	}

	if savedUser.IsVerified {
		t.Error("User should not be verified initially")
	}

	if !savedUser.CreatedAt.Equal(fixedTime) {
		t.Errorf("Expected created time %v, got %v", fixedTime, savedUser.CreatedAt)
	}

	// Verificar que el email fue enviado
	if len(emailSender.sentEmails) != 1 {
		t.Fatalf("Expected 1 email sent, got %d", len(emailSender.sentEmails))
	}

	sentEmail := emailSender.sentEmails[0]
	if sentEmail.To != "test@example.com" {
		t.Errorf("Expected email to test@example.com, got %s", sentEmail.To)
	}

	// Test de registro duplicado
	err = service.RegisterUser("test@example.com", "password123")
	if err == nil {
		t.Error("Expected error for duplicate registration")
	}
}

func TestUserVerification(t *testing.T) {
	// Configuración
	fixedTime := time.Date(2023, 1, 1, 12, 0, 0, 0, time.UTC)
	timeProvider := &MockTimeProvider{currentTime: fixedTime}
	emailSender := &MockEmailSender{}
	userStore := NewMockUserStore()

	service := NewUserRegistrationService(timeProvider, emailSender, userStore)

	// Registrar usuario primero
	service.RegisterUser("test@example.com", "password123")

	// Verificar usuario
	verifyTime := fixedTime.Add(time.Hour)
	timeProvider.currentTime = verifyTime

	err := service.VerifyUser("test@example.com")
	if err != nil {
		t.Fatalf("Expected no error, got: %v", err)
	}

	// Verificar la verificación
	user, _ := userStore.GetUser("test@example.com")
	if !user.IsVerified {
		t.Error("User should be verified")
	}

	if user.VerifiedAt == nil || !user.VerifiedAt.Equal(verifyTime) {
		t.Errorf("Expected verified time %v, got %v", verifyTime, user.VerifiedAt)
	}

	// Test de doble verificación
	err = service.VerifyUser("test@example.com")
	if err == nil {
		t.Error("Expected error for double verification")
	}
}

Este enfoque proporciona varios beneficios:

  1. Testing Aislado: Cada test se enfoca en la lógica del servicio sin dependencias externas.
  2. Entorno Controlado: Las implementaciones mock permiten simular varios escenarios.
  3. Comportamiento Predecible: Tiempo fijo y respuestas controladas hacen los tests determinísticos.
  4. Mantenimiento Fácil: Los cambios en servicios externos no rompen tus tests.

Buenas Prácticas y Anti-Patrones de Interfaces

Examinemos los qué hacer y qué no hacer del diseño de interfaces en Go.

Buenas Prácticas

1. Mantén las Interfaces Pequeñas y Enfocadas

Sigue el principio de segregación de interfaces:

small_focused_interfaces.go
// ✅ Bueno: Interfaces pequeñas y enfocadas
type Reader interface {
	Read([]byte) (int, error)
}

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

// Componer cuando sea necesario
type ReadWriter interface {
	Reader
	Writer
}

// ❌ Malo: Interfaz grande y monolítica
type BadFileManager interface {
	Read([]byte) (int, error)
	Write([]byte) (int, error)
	Seek(int64, int) (int64, error)
	Close() error
	Sync() error
	Truncate(int64) error
	Stat() (FileInfo, error)
	Chmod(FileMode) error
	// ... muchos más métodos
}

2. Define las Interfaces Donde se Usan

Coloca interfaces en el paquete consumidor, no en el paquete implementador:

interface_location.go
package user_processor

// En el paquete consumidor
type UserValidator interface {
	ValidateUser(user *User) error
}

func ProcessUser(validator UserValidator, user *User) error {
	if err := validator.ValidateUser(user); err != nil {
		return err
	}
	// Procesar usuario...
	return nil
}

3. Usa Nombres Descriptivos de Interfaces

Los nombres de interfaces deberían transmitir claramente su propósito:

descriptive_interface_names.go
// ✅ Bueno: Nombres claros y descriptivos
type EmailSender interface {
	SendEmail(to, subject, body string) error
}

type PriceCalculator interface {
	CalculatePrice(item *Item) (float64, error)
}

// ❌ Malo: Nombres genéricos o poco claros
type Manager interface {
	Do(interface{}) interface{}
}

Anti-Patrones Comunes a Evitar

Ahora veamos algunos errores comunes en el diseño de interfaces.

1. Contaminación de Interfaces

No crees interfaces para todo:

interface_pollution.go
// ❌ Malo: Interfaz innecesaria para una sola implementación
type UserService interface {
	CreateUser(name string) *User
}

type UserServiceImpl struct{}

func (us *UserServiceImpl) CreateUser(name string) *User {
	return &User{Name: name}
}

// ✅ Bueno: Usar tipo concreto directamente cuando solo hay una implementación
type UserService struct{}

func (us *UserService) CreateUser(name string) *User {
	return &User{Name: name}
}

Tener que añadir "Impl" al nombre del struct es una señal clara de que la interfaz es innecesaria. Si solo tienes una implementación y no hay planes inmediatos para otras, usa el tipo concreto directamente.

2. Interfaces Gordas

Evita interfaces con demasiados métodos:

fat_interfaces.go
// ❌ Malo: Interfaz intentando hacer demasiado
type SuperService interface {
	CreateUser(name string) error
	DeleteUser(id int) error
	SendEmail(to, subject, body string) error
	LogMessage(level, message string)
	CacheData(key string, value interface{})
	ValidateInput(input string) bool
	// ... más métodos
}

// ✅ Bueno: Separar preocupaciones en interfaces enfocadas
type UserManager interface {
	CreateUser(name string) error
	DeleteUser(id int) error
}

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

type Logger interface {
	LogMessage(level, message string)
}

3. Abstracción Prematura de Interfaces

No crees interfaces hasta que realmente necesites la abstracción:

premature_abstraction.go
// ❌ Malo: Crear interface antes de que sea necesaria
type DatabaseConnection interface {
	Query(sql string) ([]Row, error)
}

type MySQLConnection struct {}

func (mc *MySQLConnection) Query(sql string) ([]Row, error) {
	// Implementación
}

// Si solo tienes una implementación y no hay planes inmediatos para otras,
// usa el tipo concreto directamente

// ✅ Bueno: Empezar con implementación concreta
type MySQLConnection struct {}

func (mc *MySQLConnection) Query(sql string) ([]Row, error) {
	// Implementación
}

// Añadir interface más tarde cuando necesites soportar PostgreSQL, etc.

Patrones Modernos de Interfaces con Generics

Go 1.18+ introdujo generics, que funcionan muy bien con interfaces para diseños flexibles y type-safe:

generic_interfaces.go
package main

import (
	"fmt"

	"golang.org/x/exp/constraints"
)

// Interfaz genérica con parámetros de tipo
type Store[T any] interface {
	Save(item T) error
	Get(id string) (T, error)
}

// Interfaz genérica con restricciones
type Calculator[T constraints.Ordered] interface {
	Add(a, b T) T
}

// Implementación simple
type User struct {
	ID   string
	Name string
}

type MemoryStore[T any] struct {
	data map[string]T
}

func (m *MemoryStore[T]) Save(item T) error {
	if m.data == nil {
		m.data = make(map[string]T)
	}
	m.data["key"] = item
	return nil
}

func (m *MemoryStore[T]) Get(id string) (T, error) {
	item, exists := m.data[id]
	if !exists {
		var zero T
		return zero, fmt.Errorf("not found")
	}
	return item, nil
}

// Calculadora numérica con restricciones
type NumberCalc[T constraints.Ordered] struct{}

func (nc NumberCalc[T]) Add(a, b T) T {
	return a + b
}

func main() {
	// Store genérico con tipo User
	userStore := &MemoryStore[User]{}
	user := User{ID: "1", Name: "Aitana"}

	userStore.Save(user)
	fmt.Println("Generic store working!")

	// Calculadora genérica con tipo int
	calc := NumberCalc[int]{}
	sum := calc.Add(10, 20)

	fmt.Printf("10 + 20 = %d\n", sum)
}

Este enfoque moderno combina la flexibilidad de las interfaces con la seguridad de tipos de los generics, proporcionando lo mejor de ambos mundos.

Si conocer más sobre generics en Go, echa un vistazo a mi guía completa: Generics en Go: Introducción Completa a Type Parameters en Go 1.18+.

Conclusión

Las interfaces de Go representan un cambio de paradigma de la programación orientada a objetos tradicional, ofreciendo un enfoque más flexible y mantenible al diseño de software. Aprovechando la satisfacción implícita de interfaces, composición sobre herencia y diseño de interfaces enfocado, puedes construir sistemas que son al mismo tiempo potentes y fáciles de entender.

Los principios clave para recordar son:

  • Empezar Sencillo: Comenzar con tipos concretos e introducir interfaces cuando necesites la abstracción.
  • Mantener las Interfaces Pequeñas: Seguir el principio de responsabilidad única a nivel de interfaz.
  • Definir Interfaces Donde se Usan: Colocar interfaces en paquetes consumidores para promover bajo acoplamiento.
  • Aprovechar la Composición: Usar embebido de interfaces para construir comportamientos complejos a partir de componentes simples.
  • Testing con Interfaces: Aprovechar las interfaces para inyección de dependencias y mocking fácil en tests.

Ya estés construyendo microservicios, herramientas CLI o sistemas distribuidos complejos, dominar los patrones de interfaces de Go te hará un desarrollador Go más efectivo y te ayudará a escribir código que resista la prueba del tiempo.

Te Puede Interesar

  • Generics en Go: Introducción Completa a Type Parameters en Go 1.18+
  • Patrones de Concurrencia en Go: Goroutines y Channels
  • Buenas Prácticas para el Manejo de Errores en Go
Twitter iconFacebook iconLinkedIn iconPinterest iconEmail icon

Navegador de artículos
Lanzando GitHub Actions entre distintos repositoriosBuenas Prácticas para el Manejo de Errores en Go
© 2007 - 2025 Marc Nuri