Go 1.25.5 Nativo: La Capa de Comunicación en Arquitectura Hexagonal - De 0 a Experto

Go 1.25.5 Nativo: La Capa de Comunicación en Arquitectura Hexagonal - De 0 a Experto

Guía completa sobre enrutamiento nativo en Go 1.25.5. HTTP, HTTPS, REST, gRPC, JSON, XML, SOAP, WebSockets, STOMP. Arquitectura Hexagonal enfocada en endpoints. Patrones, mejores prácticas, antipatrones, casos reales. De principiante a arquitecto experto.

Por Omar Flores

Tabla de Contenidos

  1. Introducción: La Capa que Todos Olvidan
  2. Conceptos Fundamentales del Enrutamiento
  3. HTTP Nativo en Go: El Punto de Partida
  4. Arquitectura Hexagonal: Adaptadores de Comunicación
  5. REST: El Estándar de Facto
  6. gRPC: Cuando Necesitas Velocidad
  7. Más Allá de HTTP: WebSockets, STOMP y Más
  8. Patrones Profesionales y Mejores Prácticas
  9. Antipatrones: Lo Que NO Debes Hacer
  10. Casos Reales y Decisiones Arquitectónicas

Introducción: La Capa que Todos Olvidan {#introduccion}

Imagina que construiste el sistema de dominio más hermoso del mundo. Tu lógica de negocio es impecable. Tus Value Objects son inmutables y seguros. Tus Entities tienen ciclos de vida bien definidos. Tus Agregados están perfectamente delimitados.

Pero luego alguien necesita hablar con tu sistema. Alguien necesita pedir “crear una orden” o “transferir dinero” o “listar usuarios”. Y de repente, toda esa belleza se colapsa si no tienes una capa de comunicación clara.

Esto es lo que sucede en la mayoría de proyectos Go. Los desarrolladores construyen lógica de negocio hermosa y luego ponen un http.ListenAndServe() encima sin pensar. El resultado es que la capa de comunicación se convierte en un caos:

  • Lógica de negocio mezclada con parseo de HTTP
  • Errores tratados inconsistentemente
  • Validaciones duplicadas entre diferentes endpoints
  • Testing imposible
  • Cambios en protocolos (de REST a gRPC) requieren reescribir todo

Pero Go 1.25.5 te ofrece herramientas nativas extraordinarias para hacer esto bien. Y cuando lo haces bien, tu sistema es flexible, escalable, y fácil de mantener.

El Paradoxo de Go

Go es conocido como “boring” (aburrido). No tiene OOP. No tiene genéricos… bueno, ahora sí tiene genéricos en 1.18+. No tiene excepciones. Diseñadores de lenguajes alrededor del mundo miran a Go y dicen “¿Por qué?”

Pero aquí está la verdad: la “aburrida” simplicidad de Go es exactamente lo que necesitas en la capa de comunicación. Porque la capa de comunicación debe ser:

  1. Clara: Alguien debe poder leer tu código y entender inmediatamente qué sucede
  2. Rápida: Tienes milisegundos para procesar una request
  3. Confiable: Debe manejar miles de conexiones simultáneamente
  4. Predecible: Debe comportarse igual en desarrollo que en producción

Go, con su falta deliberada de trucos de lenguaje, te obliga a escribir código que cumple exactamente eso.

Lo Que Aprenderás Aquí

Esta guía te llevará desde “¿Cómo se escribe un endpoint en Go?” hasta “Soy arquitecto de sistemas de comunicación tolerantes a fallos, escalables y profesionales”.

Pero no como una referencia de API. Como una novela. Aprenderás por qué las decisiones se toman así, no solo qué hacer. Verás patrones reales que funcionan en empresas. Comprenderás cuándo usar REST vs gRPC vs WebSockets. Sabras por qué la mayoría de “mejores prácticas” online son en realidad antipatrones.

Y lo mejor: todo será en Go 1.25.5 nativo. Sin frameworks. Sin magia. Solo código que funciona.

El Viaje Visual

Aquí está el viaje que haremos:

┌─────────────────────────────────────────────────────────┐
│           CLIENTE EXTERNO (Usuario, App, API)           │
├─────────────────────────────────────────────────────────┤
│                     PROTOCOLOS                           │
│  HTTP/HTTPS  REST   gRPC   WebSockets   SOAP   XML       │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│     CAPA DE ADAPTADORES (Lo que estudiaremos)           │
│   ┌──────────────────────────────────────────────┐      │
│   │ HTTP Handler (recibe request)                │      │
│   │ ├─ Valida formato (JSON/XML/etc)            │      │
│   │ ├─ Transforma a DTOs                        │      │
│   │ └─ Devuelve respuesta en formato correcto   │      │
│   └──────────────────────────────────────────────┘      │
│                      ↓ Traduce ↓                        │
│   ┌──────────────────────────────────────────────┐      │
│   │ Application Service (Orquestación)          │      │
│   │ ├─ Valida reglas de negocio                 │      │
│   │ ├─ Coordina operaciones                     │      │
│   │ └─ Maneja transacciones                     │      │
│   └──────────────────────────────────────────────┘      │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│     CAPA DE DOMINIO (Ya lo sabes de la guía anterior)   │
│   ├─ Value Objects (Money, Email, etc)                 │
│   ├─ Entities (User, Order, etc)                       │
│   ├─ Aggregates (ShoppingCart, etc)                    │
│   └─ Domain Services (Lógica entre agregados)          │
└─────────────────────────────────────────────────────────┘

La mayoría de desarrolladores entienden el dominio. Pero la capa de adaptadores es donde sucede la magia real. Es donde tu sistema se vuelve accesible. Y hacerlo bien es un arte.

Requisitos Para Esta Guía

Asumiré que:

  • Conoces Go básico (goroutines, canales, interfaces)
  • Entiendes la arquitectura hexagonal conceptualmente (si no, lee la guía anterior sobre DDD)
  • Sabes qué es HTTP (request/response, métodos, status codes)
  • Tienes Go 1.25.5 instalado (o compatible)

Pero aquí está el secreto: incluso si no sabes nada de esto, te lo explicaré de forma que lo entiendas. Porque este es un viaje, no una referencia.

Por Qué Go 1.25.5 Específicamente

Go 1.25 fue un lanzamiento “suave”, sin cambios revolucionarios como 1.22 o 1.23. Pero 1.25.5 es estable, rápido, y tiene todas las herramientas que necesitas:

  • HTTP/2 nativo (desde hace años, pero perfeccionado)
  • Generics maduros (desde 1.18)
  • Range-over-func (desde 1.22)
  • Excelente manejo de concurrencia
  • Tooling estable para testing

No hay “frameworky” que haga la magia. Es Go puro.


Conceptos Fundamentales del Enrutamiento {#conceptos}

Antes de escribir un solo endpoint, necesitas entender algunos conceptos fundamentales que separan código profesional de código que “solo funciona”.

¿Qué Es Un Endpoint?

En la terminología moderna, un “endpoint” es un punto de acceso a tu sistema. Es donde el mundo externo habla con tu lógica de negocio.

Técnicamente, es una combinación de:

  1. Protocolo: HTTP, gRPC, WebSocket, etc
  2. Método: GET, POST, PUT, DELETE (en HTTP)
  3. Ruta: /users, /orders/123, /api/v1/transfer
  4. Handler: La función que procesa la request

Pero aquí está el punto que muchos desarrolladores pierden: un endpoint no es solo eso. Es un traductor. Traduce del mundo externo (que habla HTTP/JSON) al mundo interno (que habla dominio).

El Triángulo de Responsabilidades

Todo endpoint tiene tres responsabilidades distintas. Si las mezclas, el código se vuelve un desastre:

┌─────────────────────────────────────────────────────────┐
│                                                          │
│    1. COMUNICACIÓN (Protocol specifics)                 │
│       - Parsear JSON/XML/Protocol Buffers              │
│       - Validar formato                                 │
│       - Setear headers HTTP                            │
│       - Retornar status codes correctos                 │
│                                                          │
│    2. TRADUCCIÓN (Domain-aware)                        │
│       - Convertir DTOs a Domain Objects                │
│       - Validar negocio (no puede haber duplicados)    │
│       - Aplicar reglas (moneda válida, etc)           │
│                                                          │
│    3. ORQUESTACIÓN (Execution)                         │
│       - Llamar Domain Services                         │
│       - Manejar transacciones                          │
│       - Coordinar sideffects (emails, logs, etc)       │
│                                                          │
└─────────────────────────────────────────────────────────┘

Un endpoint profesional mantiene estas tres responsabilidades separadas y claras. Cada una ocupa su espacio. Cuando las mezclas, obtienes:

// ❌ ANTIPATRÓN: Todo mezclado
func CreateUserBadHandler(w http.ResponseWriter, r *http.Request) {
    // Responsabilidad 1: Comunicación
    body := r.Body
    // Responsabilidad 2: Traducción
    var user User
    json.NewDecoder(body).Decode(&user)
    if user.Email == "" {
        w.WriteHeader(400)
        return
    }
    // Responsabilidad 3: Orquestación
    db.Insert("users", user)
    // Responderr
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)

    // ¿Resultado? Inentendible, imposible de testear, imposible de cambiar
}

vs.

// ✓ BIEN: Responsabilidades separadas
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
    // Responsabilidad 1: Comunicación
    req, err := h.decodeRequest(r)
    if err != nil {
        h.respondWithError(w, http.StatusBadRequest, err)
        return
    }

    // Responsabilidad 2: Traducción
    user, err := h.translateToUser(req)
    if err != nil {
        h.respondWithError(w, http.StatusUnprocessableEntity, err)
        return
    }

    // Responsabilidad 3: Orquestación
    createdUser, err := h.userService.Create(r.Context(), user)
    if err != nil {
        h.respondWithError(w, http.StatusInternalServerError, err)
        return
    }

    // Responder
    h.respondWithSuccess(w, http.StatusCreated, createdUser)
}

Lo segundo es testeable, legible, y profesional.

El Concepto de “Content Negotiation”

Aquí hay algo que muchos desarrolladores no entienden: tus endpoints pueden hablar múltiples idiomas.

Un cliente podría pedir:

  • Accept: application/json → Devuelves JSON
  • Accept: application/xml → Devuelves XML
  • Accept: application/x-protobuf → Devuelves Protocol Buffers

El mismo endpoint. Diferentes formatos. Esto se llama “content negotiation” y es poderoso porque:

  1. Tu dominio no cambia
  2. Tu lógica de negocio no cambia
  3. Solo cambias cómo representas los datos al cliente

Go 1.25 hace esto trivial con interfaces:

type ResponseEncoder interface {
    Encode(w io.Writer, data interface{}) error
}

type JSONEncoder struct{}
func (e JSONEncoder) Encode(w io.Writer, data interface{}) error {
    return json.NewEncoder(w).Encode(data)
}

type XMLEncoder struct{}
func (e XMLEncoder) Encode(w io.Writer, data interface{}) error {
    return xml.NewEncoder(w).Encode(data)
}

// En tu handler:
func (h *Handler) ServeData(w http.ResponseWriter, r *http.Request) {
    data := h.service.GetData()

    encoder := h.selectEncoder(r.Header.Get("Accept"))
    encoder.Encode(w, data)
}

El Viaje de Una Request

Para entender la capa de comunicación, debes ver el viaje completo de una request:

1. CLIENTE ENVÍA REQUEST
   POST /api/v1/transfer
   Content-Type: application/json
   {
     "fromAccount": "ACC-123",
     "toAccount": "ACC-456",
     "amount": 10000
   }

2. TU SERVIDOR RECIBE (en el Listener)
   Go kernel recibe los bytes TCP

3. HTTP PARSER
   Go's `net/http` parsea el HTTP
   Identifica: método, ruta, headers, body

4. ENRUTADOR
   `http.ServeMux` o custom router
   Encuentra que ruta: POST /api/v1/transfer
   Encuentra handler: TransferHandler.Handle

5. HANDLER - RESPONSABILIDAD 1: COMUNICACIÓN
   Lee el body: r.Body ([]byte bruto)
   Parsea JSON: json.Decoder
   Transforma a DTO: TransferRequest struct

6. HANDLER - RESPONSABILIDAD 2: TRADUCCIÓN
   Valida precondiciones: amounts > 0, accounts válidas
   Transforma DTO → Domain: "Crea un Transfer aggregate"
   Valida reglas de negocio

7. HANDLER - RESPONSABILIDAD 3: ORQUESTACIÓN
   Llama: h.transferService.Execute(transfer)
   Manejar transacción ACID
   Coordina: log, email, eventos

8. HANDLER - RESPUESTA
   Obtiene resultado
   Transforma a DTO/Response
   Serializa a JSON
   Escribe headers HTTP
   Escribe body

9. HTTP RESPONSE ENVIADO AL CLIENTE
   200 OK
   Content-Type: application/json
   {
     "transferId": "TXN-789",
     "status": "completed",
     "timestamp": "2026-01-22T10:30:00Z"
   }

Entender este viaje es crucial. Porque cada paso tiene responsabilidades distintas, y confundirlas es donde suceden los problemas.

El Concepto de “Middleware Pipeline”

En profesionales, los handlers raramente trabajan solos. Están envueltos en una serie de transformaciones. Esto se llama “middleware pipeline”:

Request viene

Middleware 1: Logging (registra la request)

Middleware 2: Authentication (verifica token)

Middleware 3: Rate Limiting (verifica límites)

Middleware 4: Request Validation (estructura válida)

TU HANDLER REAL (TransferHandler.Handle)

Middleware 5: Response Logging (registra response)

Middleware 6: CORS (setea headers CORS)

Middleware 7: Compression (comprime si es necesario)

Response enviada al cliente

Esto parece complejo, pero es exactamente lo que Go hace nátivamente con funciones de orden superior. Y es elegante.


HTTP Nativo en Go: El Punto de Partida {#http-nativo}

Ahora que entiendes los conceptos, vamos a escribir código. Y comenzaremos con lo más simple: HTTP nativo.

Algunos desarrolladores dicen que “Go es muy bajo nivel” porque no tiene frameworks grandes como Rails o Django. Pero aquí está la verdad: la biblioteca estándar de Go es tan buena que la mayoría de “frameworks” Go son simplemente wrappers alrededor de net/http.

El Servidor HTTP Más Simple del Mundo

Aquí está:

// Archivo: cmd/server/main.go
// Concepto: Servidor HTTP mínimo en Go 1.25.5

package main

import (
    "fmt"
    "net/http"
)

func main() {
    // Registrar un handler para la ruta raíz
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hola, mundo!")
    })

    // Escuchar en puerto 8080
    fmt.Println("Servidor escuchando en :8080")
    http.ListenAndServe(":8080", nil)
}

Eso es todo. Corre go run main.go y tienes un servidor HTTP funcional.

Pero espera. Esto es juguete. Veamos qué está pasando realmente:

http.ListenAndServe(":8080", nil)

Esto hace varias cosas:

  1. Crea un listener TCP en puerto 8080
  2. Acepta conexiones (en goroutines automáticamente)
  3. Parsea HTTP de cada conexión
  4. Routea a handlers registrados
  5. Mantiene el servidor corriendo (bloquea forever)

Cuando pasas nil como segundo parámetro, Go usa el DefaultServeMux, que es el enrutador global. Esto es útil para código simple, pero no escalable.

El Paso a Profesional: Controlar Tu Router

Aquí está la versión profesional:

// Archivo: infrastructure/http/server.go
// Concepto: Servidor HTTP profesional, controlable, testeable

package http

import (
    "context"
    "fmt"
    "net"
    "net/http"
    "time"
)

type Server struct {
    server *http.Server
    mux    *http.ServeMux
}

func NewServer(port int) *Server {
    mux := http.NewServeMux()

    server := &http.Server{
        Addr:         fmt.Sprintf(":%d", port),
        Handler:      mux,
        ReadTimeout:  15 * time.Second,
        WriteTimeout: 15 * time.Second,
        IdleTimeout:  60 * time.Second,
    }

    return &Server{
        server: server,
        mux:    mux,
    }
}

// Register registra un handler para una ruta
func (s *Server) Register(pattern string, handler http.Handler) {
    s.mux.Handle(pattern, handler)
}

// Start inicia el servidor en una goroutine
func (s *Server) Start() error {
    go func() {
        if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            fmt.Printf("Error en servidor: %v\n", err)
        }
    }()

    return nil
}

// Stop detiene el servidor gracefully
func (s *Server) Stop(ctx context.Context) error {
    return s.server.Shutdown(ctx)
}

// IsListening verifica si el puerto está disponible
func (s *Server) IsListening() bool {
    ln, err := net.Listen("tcp", s.server.Addr)
    if err != nil {
        return true // Puerto ocupado = está escuchando
    }
    ln.Close()
    return false
}

Esto es mejor porque:

  1. Controlable: Puedes arrancar/parar el servidor
  2. Configurable: Timeouts, etc.
  3. Testeable: Puedes crear múltiples instancias en tests
  4. Graceful shutdown: No interrumpes requests en vuelo

Enrutamiento: De Simple a Profesional

La biblioteca estándar tiene http.ServeMux, que es simple pero limitado:

// Archivo: cmd/main.go - Versión Simple
mux := http.NewServeMux()
mux.HandleFunc("/users", handleUsers)
mux.HandleFunc("/users/{id}", handleUserDetail)  // Go 1.22+: soporta variables en ruta

Esto funciona, pero tienes poco control. En código profesional, construyes tu propio router o usas uno pequeño. Pero para esta guía, usaremos http.ServeMux porque es suficiente y está en stdlib.

Un Handler Real

Ahora escribamos un handler real, que sigue el patrón profesional:

// Archivo: infrastructure/http/handlers/user_handler.go
// Concepto: Handler para crear usuarios

package handlers

import (
    "encoding/json"
    "net/http"
    "myapp/application"
    "myapp/domain"
)

// UserHandler maneja requests relacionadas con usuarios
type UserHandler struct {
    userService application.UserService
}

func NewUserHandler(userService application.UserService) *UserHandler {
    return &UserHandler{
        userService: userService,
    }
}

// CreateRequest es el DTO que recibimos del cliente
type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

// UserResponse es el DTO que enviamos al cliente
type UserResponse struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Create maneja POST /users
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
    // RESPONSABILIDAD 1: COMUNICACIÓN
    // Validar método
    if r.Method != http.MethodPost {
        h.respondWithError(w, http.StatusMethodNotAllowed, "Solo POST permitido")
        return
    }

    // Parsear body
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        h.respondWithError(w, http.StatusBadRequest, "JSON inválido")
        return
    }

    // RESPONSABILIDAD 2: TRADUCCIÓN
    // Validar precondiciones básicas
    if req.Name == "" || req.Email == "" {
        h.respondWithError(w, http.StatusUnprocessableEntity, "nombre y email requeridos")
        return
    }

    // Crear domain object (con validación de negocio)
    email, err := domain.NewEmail(req.Email)
    if err != nil {
        h.respondWithError(w, http.StatusUnprocessableEntity, "email inválido")
        return
    }

    user, err := domain.NewUser(
        domain.NewUserID(), // Generar ID
        email,
        req.Name,
    )
    if err != nil {
        h.respondWithError(w, http.StatusUnprocessableEntity, err.Error())
        return
    }

    // RESPONSABILIDAD 3: ORQUESTACIÓN
    createdUser, err := h.userService.CreateUser(r.Context(), user)
    if err != nil {
        h.respondWithError(w, http.StatusInternalServerError, "error creando usuario")
        return
    }

    // RESPONDER
    response := UserResponse{
        ID:    createdUser.ID().String(),
        Name:  createdUser.Name(),
        Email: createdUser.Email().String(),
    }

    h.respondWithSuccess(w, http.StatusCreated, response)
}

// Helper methods

func (h *UserHandler) respondWithSuccess(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func (h *UserHandler) respondWithError(w http.ResponseWriter, status int, message string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(map[string]string{"error": message})
}

Observa cómo esto es testeable. Puedes mockecar userService y testear el handler sin tocar HTTP en absoluto.

Middleware: El Corazón del Pipeline

Go no tiene decoradores o anotaciones. Pero tiene algo mejor: funciones de orden superior. El patrón middleware en Go es eleganante:

// Archivo: infrastructure/http/middleware/middleware.go
// Concepto: Middleware - funciones que envuelven handlers

package middleware

import (
    "fmt"
    "net/http"
    "time"
)

// Middleware es una función que toma un handler y retorna otro handler
type Middleware func(http.Handler) http.Handler

// Logging registra todas las requests
func Logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        fmt.Printf("[%s] %s %s\n", time.Now().Format("2006-01-02 15:04:05"), r.Method, r.RequestURI)

        next.ServeHTTP(w, r)

        fmt.Printf("  → Completado en %v\n", time.Since(start))
    })
}

// Authentication verifica si el usuario está autenticado
func Authentication(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")

        if token == "" {
            w.WriteHeader(http.StatusUnauthorized)
            fmt.Fprintf(w, "Token requerido")
            return
        }

        // Aquí verificarías el token (JWT, session, etc)
        if !isValidToken(token) {
            w.WriteHeader(http.StatusUnauthorized)
            fmt.Fprintf(w, "Token inválido")
            return
        }

        // Token válido, continúa
        next.ServeHTTP(w, r)
    })
}

// RequestLogging es más sofisticado: usa ResponseWriter wrapper para capturar status
type responseWriter struct {
    http.ResponseWriter
    statusCode int
}

func (rw *responseWriter) WriteHeader(code int) {
    rw.statusCode = code
    rw.ResponseWriter.WriteHeader(code)
}

func RequestLogging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        wrapped := &responseWriter{ResponseWriter: w, statusCode: 200}

        start := time.Now()
        next.ServeHTTP(wrapped, r)

        fmt.Printf("[%d] %s %s (%v)\n",
            wrapped.statusCode,
            r.Method,
            r.RequestURI,
            time.Since(start),
        )
    })
}

// Chain combina múltiples middlewares
func Chain(handler http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        handler = middlewares[i](handler)
    }
    return handler
}

// Helper
func isValidToken(token string) bool {
    // Verificar JWT, session store, etc
    return token != ""
}

Luego usas así:

// En tu setup de rutas
handler := handlers.NewUserHandler(userService)

// Sin middleware
mux.Handle("/users/public", handler)

// Con middleware
mux.Handle("/users",
    middleware.Chain(
        http.HandlerFunc(handler.Create),
        middleware.Logging,
        middleware.Authentication,
    ),
)

El patrón Chain es hermoso porque es composable y claro.


Parte 2: Arquitectura Hexagonal - Adaptadores de Comunicación {#hexagonal-comunicacion}

Hasta ahora, escribiste handlers que mezclan HTTP con lógica. Ahora vamos a hacer lo profesional: separar completamente el protocolo de la lógica de negocio.

El Principio de Inversión de Dependencias

En hexagonal, tu dominio (el corazón) nunca conoce la infraestructura (los puertos). El flujo de dependencias siempre apunta hacia el dominio:

        CLIENTE (HTTP Request)

    [ADAPTADOR HTTP]  ← Traduce HTTP a domain objects

    [PUERTO - Interface]

    [DOMINIO] ← Nunca sabe de HTTP, nunca importa net/http

Lo contrario sería desastroso:

❌ MALO:
    DOMINIO importa "net/http"
    DOMINIO importa "encoding/json"
    DOMINIO está acoplado a HTTP

Estructura Profesional de Proyecto

myapp/
├── cmd/
│   └── server/
│       └── main.go                 # Solo orquestación, cero lógica

├── domain/                          # ❤️ CORAZÓN PURO
│   ├── user.go                     # Entity
│   ├── email.go                    # Value Object
│   ├── repository.go               # Interfaz (Puerto)
│   └── events.go                   # Domain Events

├── application/                     # Casos de uso
│   ├── create_user_service.go      # Application Service
│   └── dto.go                      # DTOs (no Domain Objects, solo transport)

├── infrastructure/
│   ├── http/                       # 🔌 ADAPTADOR HTTP
│   │   ├── server.go               # Servidor HTTP
│   │   ├── router.go               # Enrutador
│   │   ├── handlers/
│   │   │   └── user_handler.go    # Handler (Adaptador)
│   │   ├── middleware/
│   │   │   └── logging.go         # Middlewares
│   │   └── encoders/
│   │       ├── json_encoder.go    # Encoder
│   │       └── xml_encoder.go
│   │
│   ├── grpc/                       # 🔌 ADAPTADOR gRPC (futura)
│   │   ├── server.go
│   │   └── handlers/
│   │
│   ├── persistence/
│   │   └── postgres/
│   │       └── user_repository.go  # Implementación del puerto
│   │
│   └── config/
│       └── config.go

├── ports/                          # 📋 Interfaces públicas
│   ├── repository.go               # Re-export domain ports
│   └── events.go

└── go.mod

La clave: domain/ NUNCA importa infrastructure/. Infrastructure importa domain/.

El Handler como Adaptador

Ahora entiende que un handler HTTP es simplemente un adaptador. Traduce:

// Archivo: infrastructure/http/handlers/user_handler.go
// Concepto: Handler como adaptador entre HTTP y dominio

package handlers

import (
    "context"
    "encoding/json"
    "net/http"
    "myapp/application"
    "myapp/domain"
)

// UserHandler es un ADAPTADOR de HTTP
// Traduce HTTP ↔ Application ↔ Domain
type UserHandler struct {
    service application.CreateUserService
    encoder ResponseEncoder
}

func NewUserHandler(service application.CreateUserService) *UserHandler {
    return &UserHandler{
        service: service,
        encoder: JSONEncoder{},
    }
}

// CreateRequest: DTO (solo para transport, no dominio)
type CreateUserRequest struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

// UserResponse: DTO (solo para transport)
type UserResponse struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Create: POST /users
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()

    // PASO 1: RECIBIR (Comunicación)
    var req CreateUserRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        h.respondError(w, http.StatusBadRequest, "Formato inválido")
        return
    }

    // PASO 2: TRADUCIR (Traducción a dominio)
    cmd := application.CreateUserCommand{
        Name:  req.Name,
        Email: req.Email,
    }

    // PASO 3: EJECUTAR (Orquestación)
    result, err := h.service.Execute(ctx, cmd)
    if err != nil {
        h.respondError(w, http.StatusInternalServerError, err.Error())
        return
    }

    // PASO 4: RESPONDER (Comunicación)
    resp := UserResponse{
        ID:    result.UserID.String(),
        Name:  result.Name,
        Email: result.Email,
    }
    h.respondSuccess(w, http.StatusCreated, resp)
}

// Helpers
type ResponseEncoder interface {
    Encode(w http.ResponseWriter, data interface{}) error
}

type JSONEncoder struct{}

func (e JSONEncoder) Encode(w http.ResponseWriter, data interface{}) error {
    w.Header().Set("Content-Type", "application/json")
    return json.NewEncoder(w).Encode(data)
}

func (h *UserHandler) respondSuccess(w http.ResponseWriter, status int, data interface{}) {
    w.WriteHeader(status)
    h.encoder.Encode(w, data)
}

func (h *UserHandler) respondError(w http.ResponseWriter, status int, message string) {
    w.WriteHeader(status)
    h.encoder.Encode(w, map[string]string{"error": message})
}

Content Negotiation Professional

Ahora que entiendes el patrón, aquí está content negotiation real:

// Archivo: infrastructure/http/content_negotiation.go
// Concepto: Seleccionar encoder basado en Accept header

package http

import (
    "fmt"
    "io"
    "net/http"
    "encoding/json"
    "encoding/xml"
)

type ResponseEncoder interface {
    Encode(w io.Writer, data interface{}) error
}

type JSONEncoder struct{}
func (e JSONEncoder) Encode(w io.Writer, data interface{}) error {
    return json.NewEncoder(w).Encode(data)
}

type XMLEncoder struct{}
func (e XMLEncoder) Encode(w io.Writer, data interface{}) error {
    return xml.NewEncoder(w).Encode(data)
}

// SelectEncoder basado en Accept header
func SelectEncoder(r *http.Request) ResponseEncoder {
    accept := r.Header.Get("Accept")

    switch accept {
    case "application/xml":
        return XMLEncoder{}
    case "application/json":
        fallthrough
    default:
        return JSONEncoder{}
    }
}

// En tu handler:
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
    encoder := SelectEncoder(r)
    user := h.service.GetUser()

    if err := encoder.Encode(w, user); err != nil {
        fmt.Fprintf(w, "Error: %v", err)
    }
}

Parte 3: REST Profesional {#rest}

REST no es “devolver JSON en HTTP”. REST es una arquitectura con principios específicos. Aquí está cómo hacerlo profesional en Go 1.25.5.

Los Principios de REST (La Verdad)

La mayoría de APIs dicen ser “RESTful” pero no lo son. REST requiere:

  1. Recursos (nouns, no verbs): /users, /orders, /transfers
  2. Métodos HTTP correctos: GET (read), POST (create), PUT (replace), PATCH (update), DELETE
  3. Status codes significativos: 200, 201, 204, 400, 401, 403, 404, 409, 422, 500, etc.
  4. Hypermedia (HATEOAS): Links para descubrir recursos (lo que casi nadie hace)

Estructura de Rutas Profesional

// Archivo: infrastructure/http/router.go
// Concepto: Enrutador REST profesional

package http

import (
    "net/http"
    "myapp/infrastructure/http/handlers"
)

func SetupRoutes(mux *http.ServeMux, h *handlers.Handlers) {
    // USUARIOS (Resource: users)
    mux.HandleFunc("GET /users", h.User.List)           // Listar todos
    mux.HandleFunc("GET /users/{id}", h.User.Get)       // Obtener uno
    mux.HandleFunc("POST /users", h.User.Create)        // Crear
    mux.HandleFunc("PUT /users/{id}", h.User.Update)    // Reemplazar
    mux.HandleFunc("PATCH /users/{id}", h.User.Patch)   // Actualizar parcial
    mux.HandleFunc("DELETE /users/{id}", h.User.Delete) // Eliminar

    // ÓRDENES (Resource: orders)
    mux.HandleFunc("GET /orders", h.Order.List)
    mux.HandleFunc("GET /orders/{id}", h.Order.Get)
    mux.HandleFunc("POST /orders", h.Order.Create)
    // ... etc

    // SALUD (Health check)
    mux.HandleFunc("GET /health", h.Health.Check)

    // MÉTRICAS (para monitoreo)
    mux.HandleFunc("GET /metrics", h.Metrics.Dump)
}

Go 1.22+ soporta sintaxis GET /path, que es hermosa.

Validación RESTful

// Archivo: infrastructure/http/validation/validation.go
// Concepto: Validación centralizada

package validation

import (
    "errors"
    "fmt"
    "myapp/domain"
)

// ValidationError agrupa múltiples errores
type ValidationError struct {
    Fields map[string]string // field → error message
}

func (e ValidationError) Error() string {
    if len(e.Fields) == 0 {
        return "validación fallida"
    }
    return fmt.Sprintf("validación: %v", e.Fields)
}

// ValidateCreateUserRequest valida el request
func ValidateCreateUserRequest(req CreateUserRequest) error {
    errors := make(map[string]string)

    if req.Name == "" {
        errors["name"] = "requerido"
    } else if len(req.Name) < 3 {
        errors["name"] = "mínimo 3 caracteres"
    } else if len(req.Name) > 100 {
        errors["name"] = "máximo 100 caracteres"
    }

    if req.Email == "" {
        errors["email"] = "requerido"
    } else if _, err := domain.NewEmail(req.Email); err != nil {
        errors["email"] = "formato inválido"
    }

    if len(errors) > 0 {
        return ValidationError{Fields: errors}
    }

    return nil
}

// En tu handler:
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
    var req CreateUserRequest
    json.NewDecoder(r.Body).Decode(&req)

    if err := validation.ValidateCreateUserRequest(req); err != nil {
        if ve, ok := err.(validation.ValidationError); ok {
            h.respondError(w, http.StatusUnprocessableEntity, ve.Fields)
        } else {
            h.respondError(w, http.StatusBadRequest, err.Error())
        }
        return
    }

    // Continuar...
}

Manejo de Errores Consistente

// Archivo: infrastructure/http/error_handler.go
// Concepto: Traducir domain errors a HTTP responses

package http

import (
    "net/http"
    "myapp/domain"
)

// ErrorResponse es el formato estándar de error
type ErrorResponse struct {
    Code    string                 `json:"code"`
    Message string                 `json:"message"`
    Details map[string]interface{} `json:"details,omitempty"`
}

// HandleError traduce domain errors a HTTP
func (h *Handler) HandleError(w http.ResponseWriter, err error) {
    if err == nil {
        return
    }

    // Domain errors
    if err == domain.ErrUserNotFound {
        h.respondErrorWithCode(w, http.StatusNotFound, "USER_NOT_FOUND", "Usuario no encontrado")
        return
    }

    if err == domain.ErrDuplicateEmail {
        h.respondErrorWithCode(w, http.StatusConflict, "DUPLICATE_EMAIL", "Email ya existe")
        return
    }

    if err == domain.ErrInsufficientFunds {
        h.respondErrorWithCode(w, http.StatusUnprocessableEntity, "INSUFFICIENT_FUNDS", "Fondos insuficientes")
        return
    }

    // Unknown error
    h.respondErrorWithCode(w, http.StatusInternalServerError, "INTERNAL_ERROR", "Error interno")
}

func (h *Handler) respondErrorWithCode(w http.ResponseWriter, status int, code string, message string) {
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(ErrorResponse{
        Code:    code,
        Message: message,
    })
}

Versionamiento de API

// Archivo: infrastructure/http/handlers/api_version.go
// Concepto: Versionamiento via URL path

package handlers

import (
    "net/http"
)

// V1 handlers (legacy)
type V1UserHandler struct {
    service UserService
}

func (h *V1UserHandler) Create(w http.ResponseWriter, r *http.Request) {
    // Implementación V1 (quizás diferente response format)
}

// V2 handlers (nuevo, mejorado)
type V2UserHandler struct {
    service UserService
}

func (h *V2UserHandler) Create(w http.ResponseWriter, r *http.Request) {
    // Implementación V2 (response format mejorado)
}

// En router:
func SetupRoutes(mux *http.ServeMux) {
    v1User := &V1UserHandler{}
    v2User := &V2UserHandler{}

    mux.HandleFunc("POST /api/v1/users", v1User.Create)
    mux.HandleFunc("POST /api/v2/users", v2User.Create)
}

Parte 4: gRPC - Cuando Necesitas Velocidad {#grpc}

REST es hermoso pero lento. gRPC es rápido pero complejo. Aquí está cómo hacerlo en Go 1.25.5.

Qué es gRPC (La Verdad Simple)

gRPC = Google RPC = Protocol Buffers + HTTP/2 + Go generador de código.

Es una forma de llamar funciones remotas con estas características:

  • Rápido: Binario (no JSON text)
  • Tipado: Protocol Buffers enforce tipos
  • Bidireccional: Streaming en ambas direcciones
  • Con estado: Conexión persistent HTTP/2
  • Generado: El compilador protoc genera código boilerplate

Definir un Servicio gRPC

// Archivo: proto/user.proto
// Concepto: Definición de servicio gRPC

syntax = "proto3";

package user;

option go_package = "myapp/proto/user";

// Mensajes (tipos de datos)
message CreateUserRequest {
    string name = 1;
    string email = 2;
}

message UserResponse {
    string id = 1;
    string name = 2;
    string email = 3;
}

// Servicio (métodos RPC)
service UserService {
    rpc Create(CreateUserRequest) returns (UserResponse);
    rpc Get(GetUserRequest) returns (UserResponse);
    rpc List(ListUsersRequest) returns (stream UserResponse);
}

message GetUserRequest {
    string id = 1;
}

message ListUsersRequest {
    int32 page = 1;
    int32 limit = 2;
}

Luego compilas:

protoc --go_out=. --go-grpc_out=. proto/user.proto

Eso genera código Go.

Implementar un Servidor gRPC

// Archivo: infrastructure/grpc/user_service.go
// Concepto: Implementar servicio gRPC

package grpc

import (
    "context"
    "myapp/application"
    "myapp/proto/user"
)

type UserServiceServer struct {
    user.UnimplementedUserServiceServer
    service application.CreateUserService
}

func NewUserServiceServer(service application.CreateUserService) *UserServiceServer {
    return &UserServiceServer{
        service: service,
    }
}

// Implementar Create RPC
func (s *UserServiceServer) Create(ctx context.Context, req *user.CreateUserRequest) (*user.UserResponse, error) {
    cmd := application.CreateUserCommand{
        Name:  req.Name,
        Email: req.Email,
    }

    result, err := s.service.Execute(ctx, cmd)
    if err != nil {
        return nil, err
    }

    return &user.UserResponse{
        Id:    result.UserID.String(),
        Name:  result.Name,
        Email: result.Email,
    }, nil
}

// Implementar Get RPC
func (s *UserServiceServer) Get(ctx context.Context, req *user.GetUserRequest) (*user.UserResponse, error) {
    // Implementación...
    return nil, nil
}

// Implementar List RPC (streaming)
func (s *UserServiceServer) List(req *user.ListUsersRequest, stream user.UserService_ListServer) error {
    users := s.service.List(req.Page, req.Limit)

    for _, u := range users {
        if err := stream.Send(&user.UserResponse{
            Id:    u.ID.String(),
            Name:  u.Name,
            Email: u.Email,
        }); err != nil {
            return err
        }
    }

    return nil
}

Iniciar Servidor gRPC

// Archivo: cmd/server/main.go
// Concepto: Arrancar servidor gRPC

package main

import (
    "fmt"
    "net"

    "google.golang.org/grpc"
    "myapp/application"
    "myapp/infrastructure/grpc"
    "myapp/proto/user"
)

func main() {
    // Crear listener en puerto 50051
    listener, err := net.Listen("tcp", ":50051")
    if err != nil {
        panic(err)
    }
    defer listener.Close()

    // Crear servidor gRPC
    grpcServer := grpc.NewServer()

    // Registrar servicio
    userService := application.NewCreateUserService()
    userGrpc := grpc.NewUserServiceServer(userService)
    user.RegisterUserServiceServer(grpcServer, userGrpc)

    // Escuchar
    fmt.Println("gRPC servidor en :50051")
    if err := grpcServer.Serve(listener); err != nil {
        panic(err)
    }
}

Cliente gRPC

// Archivo: clients/grpc_client.go
// Concepto: Cliente gRPC

package main

import (
    "context"
    "fmt"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    "myapp/proto/user"
)

func main() {
    // Conectar al servidor
    conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        panic(err)
    }
    defer conn.Close()

    // Crear cliente
    client := user.NewUserServiceClient(conn)

    // Llamar RPC
    resp, err := client.Create(context.Background(), &user.CreateUserRequest{
        Name:  "John",
        Email: "john@example.com",
    })
    if err != nil {
        panic(err)
    }

    fmt.Printf("Usuario creado: %s\n", resp.Id)
}

HTTP/2 + gRPC Gateway (Lo Mejor de Ambos)

Aquí está el truco: puedes tener AMBOS HTTP/REST y gRPC en el mismo servidor:

// Archivo: cmd/server/main.go
// Concepto: Servidor dual HTTP + gRPC

package main

import (
    "net"
    "net/http"

    "google.golang.org/grpc"
    "myapp/infrastructure/grpc"
    "myapp/infrastructure/http"
)

func main() {
    // Crear listener que soporte ambos
    listener, _ := net.Listen("tcp", ":8080")

    // Iniciar servidor dual
    go startGRPCServer(listener)
    go startHTTPServer()

    select {} // Bloquear forever
}

func startGRPCServer(listener net.Listener) {
    grpcServer := grpc.NewServer()
    // Registrar servicios gRPC...
    grpcServer.Serve(listener)
}

func startHTTPServer() {
    mux := http.NewServeMux()
    // Registrar handlers HTTP...
    http.ListenAndServe(":8081", mux)
}

Parte 5: Más Allá de HTTP - WebSockets, STOMP y Más {#mas-alla}

No todo es HTTP request-response. A veces necesitas bidireccional en tiempo real.

WebSockets - Comunicación Bidireccional

WebSocket es HTTP que se actualiza a protocolo persistente. Perfecto para chat, notificaciones, datos en tiempo real.

// Archivo: infrastructure/websocket/handler.go
// Concepto: Handler WebSocket

package websocket

import (
    "net/http"
    "github.com/gorilla/websocket"
    "myapp/domain"
)

var upgrader = websocket.Upgrader{
    ReadBufferSize:  1024,
    WriteBufferSize: 1024,
    CheckOrigin: func(r *http.Request) bool {
        // En producción: valida origen
        return true
    },
}

type Hub struct {
    clients    map[*Client]bool
    broadcast chan interface{}
    register   chan *Client
    unregister chan *Client
}

type Client struct {
    hub      *Hub
    conn     *websocket.Conn
    send     chan interface{}
}

func NewHub() *Hub {
    return &Hub{
        clients:    make(map[*Client]bool),
        broadcast: make(chan interface{}),
        register:   make(chan *Client),
        unregister: make(chan *Client),
    }
}

func (h *Hub) Run() {
    for {
        select {
        case client := <-h.register:
            h.clients[client] = true

        case client := <-h.unregister:
            if _, ok := h.clients[client]; ok {
                delete(h.clients, client)
                close(client.send)
            }

        case message := <-h.broadcast:
            for client := range h.clients {
                select {
                case client.send <- message:
                default:
                    close(client.send)
                    delete(h.clients, client)
                }
            }
        }
    }
}

func (h *Hub) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
    conn, _ := upgrader.Upgrade(w, r, nil)

    client := &Client{
        hub:  h,
        conn: conn,
        send: make(chan interface{}, 256),
    }

    h.register <- client

    go client.writePump()
    go client.readPump()
}

func (c *Client) readPump() {
    defer func() {
        c.hub.unregister <- c
        c.conn.Close()
    }()

    for {
        var message map[string]interface{}
        if err := c.conn.ReadJSON(&message); err != nil {
            break
        }

        // Procesar mensaje
        c.hub.broadcast <- message
    }
}

func (c *Client) writePump() {
    defer c.conn.Close()

    for message := range c.send {
        if err := c.conn.WriteJSON(message); err != nil {
            return
        }
    }
}

STOMP - Protocolo Ligero sobre WebSocket

STOMP es ideal para message brokers.

// Archivo: infrastructure/stomp/handler.go
// Concepto: Handler STOMP (simplificado)

package stomp

import (
    "fmt"
    "strings"
)

type StompFrame struct {
    Command string
    Headers map[string]string
    Body    string
}

func ParseFrame(data string) (*StompFrame, error) {
    parts := strings.Split(data, "\n")

    frame := &StompFrame{
        Command: parts[0],
        Headers: make(map[string]string),
    }

    // Parsear headers
    i := 1
    for i < len(parts) && parts[i] != "" {
        header := strings.Split(parts[i], ":")
        if len(header) == 2 {
            frame.Headers[header[0]] = header[1]
        }
        i++
    }

    // Parsear body
    if i+1 < len(parts) {
        frame.Body = strings.Join(parts[i+1:], "\n")
    }

    return frame, nil
}

func (f *StompFrame) String() string {
    result := f.Command + "\n"
    for k, v := range f.Headers {
        result += fmt.Sprintf("%s:%s\n", k, v)
    }
    result += "\n" + f.Body + "\x00"
    return result
}

JSON-RPC - RPC sobre HTTP/WebSocket

JSON-RPC es elegante para APIs RPC:

// Archivo: infrastructure/jsonrpc/handler.go
// Concepto: JSON-RPC handler

package jsonrpc

import (
    "encoding/json"
    "net/http"
)

type Request struct {
    JSONRPC string          `json:"jsonrpc"`
    Method  string          `json:"method"`
    Params  json.RawMessage `json:"params"`
    ID      int             `json:"id"`
}

type Response struct {
    JSONRPC string      `json:"jsonrpc"`
    Result  interface{} `json:"result,omitempty"`
    Error   *Error      `json:"error,omitempty"`
    ID      int         `json:"id"`
}

type Error struct {
    Code    int    `json:"code"`
    Message string `json:"message"`
}

type Handler struct {
    methods map[string]func(interface{}) (interface{}, error)
}

func NewHandler() *Handler {
    return &Handler{
        methods: make(map[string]func(interface{}) (interface{}, error)),
    }
}

func (h *Handler) Register(method string, fn func(interface{}) (interface{}, error)) {
    h.methods[method] = fn
}

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    var req Request
    json.NewDecoder(r.Body).Decode(&req)

    method, ok := h.methods[req.Method]
    if !ok {
        resp := Response{
            JSONRPC: "2.0",
            Error:   &Error{Code: -32601, Message: "Method not found"},
            ID:      req.ID,
        }
        json.NewEncoder(w).Encode(resp)
        return
    }

    result, err := method(req.Params)

    resp := Response{
        JSONRPC: "2.0",
        ID:      req.ID,
    }

    if err != nil {
        resp.Error = &Error{Code: -32603, Message: err.Error()}
    } else {
        resp.Result = result
    }

    json.NewEncoder(w).Encode(resp)
}

Parte 6: Patrones Profesionales y Mejores Prácticas {#patrones}

Aquí están los patrones que separan código profesional de código que “funciona”.

Patrón 1: Circuit Breaker

Protege tu sistema cuando servicios externos fallan:

// Archivo: infrastructure/patterns/circuit_breaker.go
// Concepto: Circuit Breaker para llamadas externas

package patterns

import (
    "errors"
    "sync"
    "time"
)

type CircuitBreakerState string

const (
    Closed   CircuitBreakerState = "closed"   // Normal
    Open     CircuitBreakerState = "open"     // Fallos recientes
    HalfOpen CircuitBreakerState = "half-open" // Probando recuperación
)

type CircuitBreaker struct {
    state       CircuitBreakerState
    failures    int
    lastFailure time.Time
    threshold   int
    timeout     time.Duration
    mu          sync.Mutex
}

func NewCircuitBreaker(threshold int, timeout time.Duration) *CircuitBreaker {
    return &CircuitBreaker{
        state:     Closed,
        threshold: threshold,
        timeout:   timeout,
    }
}

func (cb *CircuitBreaker) Call(fn func() error) error {
    cb.mu.Lock()
    defer cb.mu.Unlock()

    // Si está open y pasó timeout, intentar recuperar
    if cb.state == Open && time.Since(cb.lastFailure) > cb.timeout {
        cb.state = HalfOpen
        cb.failures = 0
    }

    if cb.state == Open {
        return errors.New("circuit breaker is open")
    }

    // Intentar
    err := fn()

    if err != nil {
        cb.failures++
        cb.lastFailure = time.Now()

        if cb.failures >= cb.threshold {
            cb.state = Open
        }

        return err
    }

    // Éxito: resetear
    cb.failures = 0
    cb.state = Closed
    return nil
}

Patrón 2: Retry con Backoff Exponencial

// Archivo: infrastructure/patterns/retry.go
// Concepto: Retry inteligente

package patterns

import (
    "time"
)

func RetryWithBackoff(maxAttempts int, fn func() error) error {
    var lastErr error
    backoff := 100 * time.Millisecond

    for attempt := 0; attempt < maxAttempts; attempt++ {
        lastErr = fn()
        if lastErr == nil {
            return nil
        }

        if attempt < maxAttempts-1 {
            time.Sleep(backoff)
            backoff *= 2 // Exponencial
            if backoff > 10*time.Second {
                backoff = 10 * time.Second // Cap
            }
        }
    }

    return lastErr
}

Patrón 3: Timeout

// Archivo: infrastructure/patterns/timeout.go
// Concepto: Operaciones con timeout

package patterns

import (
    "context"
    "time"
)

func WithTimeout(fn func(context.Context) error, timeout time.Duration) error {
    ctx, cancel := context.WithTimeout(context.Background(), timeout)
    defer cancel()

    done := make(chan error, 1)
    go func() {
        done <- fn(ctx)
    }()

    select {
    case err := <-done:
        return err
    case <-ctx.Done():
        return ctx.Err() // Timeout
    }
}

Patrón 4: Rate Limiting

// Archivo: infrastructure/http/middleware/rate_limit.go
// Concepto: Limitar requests por segundo

package middleware

import (
    "net/http"
    "sync"
    "time"
)

type RateLimiter struct {
    requestsPerSecond int
    tokens            int
    lastRefill        time.Time
    mu                sync.Mutex
}

func NewRateLimiter(rps int) *RateLimiter {
    return &RateLimiter{
        requestsPerSecond: rps,
        tokens:            rps,
        lastRefill:        time.Now(),
    }
}

func (rl *RateLimiter) Allow() bool {
    rl.mu.Lock()
    defer rl.mu.Unlock()

    now := time.Now()
    elapsed := now.Sub(rl.lastRefill).Seconds()
    tokensToAdd := int(elapsed) * rl.requestsPerSecond

    rl.tokens += tokensToAdd
    if rl.tokens > rl.requestsPerSecond {
        rl.tokens = rl.requestsPerSecond
    }
    rl.lastRefill = now

    if rl.tokens > 0 {
        rl.tokens--
        return true
    }

    return false
}

func RateLimitMiddleware(limiter *RateLimiter) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            if !limiter.Allow() {
                w.WriteHeader(http.StatusTooManyRequests)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

Parte 7: Antipatrones - Lo Que NO Debes Hacer {#antipatrones}

Antipatrón 1: Mezclar Protocolo con Dominio

// ❌ MALO: net/http en el dominio
package domain

import "net/http"

func (u *User) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // El dominio NUNCA debe saber de HTTP
}

// ✓ BIEN: Separación clara
package domain

type User struct {
    id    UserID
    email Email
}

// En infrastructure/
package http

func (h *Handler) ServeUser(w http.ResponseWriter, r *http.Request) {
    user := domain.User{}
    // Traducir a respuesta HTTP
}

Antipatrón 2: Sin Timeout

// ❌ MALO: Sin protección
func (h *Handler) GetData(w http.ResponseWriter, r *http.Request) {
    data := h.service.GetFromDatabase() // ¿Cuánto tarda? ¿Infinito?
}

// ✓ BIEN: Con timeout
func (h *Handler) GetData(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
    defer cancel()

    data, err := h.service.GetFromDatabase(ctx)
    if err == context.DeadlineExceeded {
        h.respondError(w, http.StatusGatewayTimeout, "Timeout")
        return
    }
}

Antipatrón 3: Responder Después de Escribir

// ❌ MALO: No se puede escribir dos veces
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.WriteHeader(http.StatusBadRequest) // Demasiado tarde, ignorado
}

// ✓ BIEN: Decide primero
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
    status := http.StatusOK

    if err != nil {
        status = http.StatusBadRequest
    }

    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

Antipatrón 4: Global State

// ❌ MALO: Variables globales
var db *sql.DB // Global
var cache map[string]interface{} // Global

func Handler(w http.ResponseWriter, r *http.Request) {
    data := db.Query(...) // De dónde vino db?
    cache["key"] = data   // Acceso sin sincronización
}

// ✓ BIEN: Inyección de dependencias
type Handler struct {
    db    *sql.DB
    cache Cache
}

func (h *Handler) Handle(w http.ResponseWriter, r *http.Request) {
    data := h.db.Query(...)
    h.cache.Set("key", data)
}

Antipatrón 5: Errores Silenciosos

// ❌ MALO: Ignorar errores
func (h *Handler) Process(w http.ResponseWriter, r *http.Request) {
    user := h.service.GetUser(id) // ¿Qué pasa si falla?
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(user) // Puede ser nil
}

// ✓ BIEN: Manejo explícito
func (h *Handler) Process(w http.ResponseWriter, r *http.Request) {
    user, err := h.service.GetUser(id)
    if err != nil {
        h.respondError(w, http.StatusNotFound, "Usuario no encontrado")
        return
    }
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(user)
}

Parte 8: Casos Reales y Decisiones Arquitectónicas {#casos-reales}

Caso 1: Plataforma de Pagos (REST + gRPC)

Requisito: Clientes web usan REST. Servicios internos usan gRPC.

Decisión: Servidor dual en puerto 8080 (HTTP/REST) y puerto 50051 (gRPC).

// cmd/payment-service/main.go

func main() {
    // Setup
    userService := application.NewUserService()
    paymentService := application.NewPaymentService()

    // HTTP + REST
    go func() {
        httpServer := newHTTPServer(8080, userService, paymentService)
        httpServer.Start()
    }()

    // gRPC
    go func() {
        grpcServer := newGRPCServer(50051, userService, paymentService)
        grpcServer.Start()
    }()

    // Mantener corriendo
    select {}
}

Caso 2: Notificaciones en Tiempo Real (WebSocket)

Requisito: Usuarios conectados reciben notificaciones instantly.

Decisión: WebSocket + Hub pattern.

// cmd/notification-service/main.go

func main() {
    hub := websocket.NewHub()
    go hub.Run()

    mux := http.NewServeMux()
    mux.HandleFunc("/ws", hub.HandleWebSocket)

    // Cuando sucede evento, broadcast a todos
    go func() {
        for event := range eventChannel {
            hub.broadcast <- event
        }
    }()

    http.ListenAndServe(":8080", mux)
}

Caso 3: API Pública Escalable (REST + Caching + CDN)

Requisito: Millones de requests/segundo, datos que no cambian frecuentemente.

Decisión: REST + Etag + Cache-Control headers + CDN.

// infrastructure/http/caching_handler.go

func (h *Handler) GetProduct(w http.ResponseWriter, r *http.Request) {
    productID := r.URL.Query().Get("id")
    product := h.cache.Get(productID)

    if product != nil {
        // Verificar If-None-Match (Etag)
        etag := h.computeEtag(product)
        if r.Header.Get("If-None-Match") == etag {
            w.WriteHeader(http.StatusNotModified)
            return
        }

        // Cacheable por 1 hora
        w.Header().Set("Cache-Control", "public, max-age=3600")
        w.Header().Set("ETag", etag)
        json.NewEncoder(w).Encode(product)
        return
    }

    w.WriteHeader(http.StatusNotFound)
}

Decisión: REST vs gRPC vs WebSocket

┌─────────────────────────────────────────────────────────┐
│ Elige según...                                           │
├─────────────────────────────────────────────────────────┤
│                                                          │
│ REST:                                                   │
│   ✓ Clientes públicos (web, mobile, third-party)      │
│   ✓ APIs documentadas y estables                       │
│   ✓ Caching necesario                                  │
│   ✓ Simple, sin dependencias                           │
│   ✗ Lento para datos grandes                           │
│   ✗ Sin streaming real                                 │
│                                                          │
│ gRPC:                                                   │
│   ✓ Servicios internos (microservicios)               │
│   ✓ Alto volumen, baja latencia                        │
│   ✓ Streaming bidireccional                           │
│   ✓ Generación automática de clientes                 │
│   ✗ No es human-readable                              │
│   ✗ Curva de aprendizaje                              │
│                                                          │
│ WebSocket:                                             │
│   ✓ Comunicación en tiempo real                        │
│   ✓ Notificaciones push                               │
│   ✓ Chat, colaboración real-time                      │
│   ✗ Stateful (complejo de escalar)                    │
│   ✗ Consume recursos                                  │
│                                                          │
└─────────────────────────────────────────────────────────┘

Conclusión: La Capa de Comunicación Profesional

Hemos recorrido un viaje desde HTTP básico hasta sistemas profesionales escalables.

Checklist de Profesionalismo

Antes de enviar a producción, verifica:

ARQUITECTURA
☐ Dominio no importa infrastructure/
☐ Handlers son adaptadores, no lógica
☐ DTOs para transport, Domain Objects para negocio
☐ Errores del dominio traducen a HTTP correctamente

HTTP/REST
☐ Rutas usan nouns (recursos), no verbs
☐ Status codes correctos (200, 201, 400, 404, 500)
☐ Content negotiation implementada (JSON, XML, etc)
☐ Validación centralizada

SEGURIDAD
☐ Timeout en todos los requests
☐ Rate limiting en endpoints públicos
☐ Authentication y authorization
☐ CORS configurado correctamente

OBSERVABILIDAD
☐ Logging estructurado (quién llamó, qué pasó, cuánto tardó)
☐ Métricas (requests/segundo, errores, latencia)
☐ Health check endpoint
☐ Distributed tracing (si tienes múltiples servicios)

TESTING
☐ Handlers testeables (sin mock de http.ResponseWriter)
☐ Mocks de servicios
☐ Tests de integración
☐ Tests de contrato gRPC si aplica

DEPLOYMENT
☐ Graceful shutdown (no interrumpir requests)
☐ Readiness/liveness probes
☐ Configuración vía environment variables
☐ Logs a stdout (para contenedores)

El Viaje Continúa

Esta guía te llevó de “¿Cómo escribo un endpoint?” a “Soy arquitecto de sistemas de comunicación profesionales”.

Pero el conocimiento es solo la mitad. La otra mitad es práctica. Toma estos patrones, aplícalos, fallos, aprende, repite.

Go 1.25.5 te da todas las herramientas. Lo que queda es decisión de arquitectura.


Recursos y Referencias


FIN DE LA GUÍA

Comenzaste aquí sin saber nada. Ahora entiendes por qué las decisiones arquitectónicas en la capa de comunicación importan. Entiendes cómo mantener tu dominio puro. Entiendes cómo escalar.

Eso es lo que importa.

Tags

#Go #HTTP #gRPC #REST #WebSockets #Architecture #Hexagonal #Go 1.25.5 #Backend #API Design