Un paquete es un directorio que agrupa archivos con el mismo nombre de espacio, definiendo la visibilidad de sus elementos (exportados vs. no exportados). Un módulo es la unidad de distribución y versionado; un módulo contiene uno o más paquetes y se define mediante un archivo go.mod.
La razón de esta distinción radica en la gestión de dependencias y la complejidad. Los paquetes organizan el código para facilitar la lectura y el encapsulamiento, mientras que los módulos gestionan el ciclo de vida del software. Debes crear paquetes cuando una parte de tu código tiene una responsabilidad distinta (por ejemplo, lógica de negocio vs. acceso a base de datos) o cuando quieres que otros usuarios puedan importar tus funciones. Usarás módulos cuando necesites que una parte del proyecto tenga un ciclo de release independiente, cuando estés desarrollando un plugin o cuando quieras que una librería evolucione en una versión distinta a la del proyecto principal.
Si divides mal, te enfrentarás a dos problemas distintos: si fragmentas demasiado los paquetes, te golpearás contra los ciclos de importación (import cycles), un error de compilación que Go prohíbe estrictamente; si fragmentas demasiado los módulos, crearás una pesadilla de dependencias donde la gestión de versiones de go.mod se vuelve inmanejable y los conflictos de versiones de dependencias indirectas te impedirán compilar el proyecto.
package example_test
import (
"errors"
"fmt"
"testing"
)
// --- LÓGICA DE DOMINIO (Simulando el paquete 'domain') ---
// En un proyecto real, esto viviría en /pkg/domain/payment.go
type PaymentStatus string
const (
StatusSuccess PaymentStatus = "success"
StatusFailed PaymentStatus = "failed"
)
// PaymentProcessor es una interfaz. Esto es clave para el desacoplamiento.
// Al usar interfaces, el paquete que la consume no necesita importar
// el paquete que tiene la implementación concreta.
type PaymentProcessor interface {
Process(amount float64) (PaymentStatus, error)
}
// --- LÓGICA DE APLICACIÓN (Simulando el paquete 'service') ---
// En un proyecto real, esto viviría en /internal/service/order.go
type OrderService struct {
processor PaymentProcessor
}
func NewOrderService(p PaymentProcessor) *OrderService {
return &OrderService{processor: p}
}
func (s *OrderService) PlaceOrder(amount float64) error {
if amount <= 0 {
return errors.New("el monto debe ser mayor a cero")
}
status, err := s.processor.Process(amount)
if err != nil {
return fmt.Errorf("error en el pago: %w", err)
}
if status != StatusSuccess {
return errors.New("el pago fue rechazado")
}
return nil
}
// --- TEST DE INTEGRACIÓN (Simulando el uso del sistema) ---
// MockPayment es nuestra implementación de prueba para evitar
// depender de una implementación real de red o base de datos.
type MockPayment struct {
shouldFail bool
}
func (m *MockPayment) Process(amount float64) (PaymentStatus, error) {
if m.shouldFail {
return StatusFailed, nil
}
return StatusSuccess, nil
}
func TestOrderService_PlaceOrder(t *testing.T) {
tests := []struct {
name string
amount float64
shouldFail bool
expectedError bool
}{
{"Orden válida", 100.0, false, false},
{"Monto inválido", 0.0, false, true},
{"Pago rechazado", 50.0, true, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mock := &MockPayment{shouldFail: tt.shouldFail}
svc := NewOrderService(mock)
err := svc.PlaceOrder(tt.amount)
if (err != nil) != tt.expectedError {
t.Errorf("PlaceOrder() error = %v, errorExpected %v", err, tt.expectedError)
}
})
}
}
Desglose de la arquitectura
En el ejemplo, hemos diseñado el código pensando en la futura división en paquetes. Observa la interfaz PaymentProcessor definida en la sección de “dominio”. Esta es la herramienta más potente para evitar ciclos de importación.
Cuando OrderService necesita realizar un pago, no importa directamente un paquete concreto como stripe o paypal. En su lugar, depende de la interfaz PaymentProcessor. Esto permite que, si mañana decidimos separar la lógica de pagos en un módulo independiente, el módulo de OrderService no necesite saber nada de la implementación, solo de la firma del método Process.
En el test, el MockPayment implementa esa interfaz. En una estructura de paquetes real, esto nos permite testear la lógica de PlaceOrder sin necesidad de importar paquetes pesados de infraestructura, manteniendo nuestros tests rápidos y desacoplados. El OrderService es el “orquestador” y su única responsabilidad es coordinar el flujo, no saber cómo se procesa el dinero.
El error frecuente
El error más común al intentar organizar paquetes es crear una dependencia circular. En Go, esto es un error de compilación fatal.
// --- ERROR: CICLO DE IMPORTACIÓN ---
// paquete "usuario"
package usuario
import "proyecto/pedido"
type Usuario struct {
UltimoPedido *pedido.Pedido // 'usuario' importa 'pedido'
}
// ---
// paquete "pedido"
package pedido
import "proyecto/usuario"
type Pedido struct {
Dueño *usuario.Usuario // 'pedido' importa 'usuario' -> ¡BOOM! CICLO
}
Si te encuentras en esta situación, no intentes arreglarlo moviendo cosas a otros paquetes al azar. La solución profesional es extraer la dependencia compartida a una tercera interfaz o a un paquete de “modelos” o “tipos” mucho más básico que no importe a nadie más. La regla de oro es: la dependencia siempre debe fluir hacia abajo, de lo que “usa” a lo que “es usado”, nunca de forma circular.
N° 106