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.
Tabla de Contenidos
- Introducción: La Capa que Todos Olvidan
- Conceptos Fundamentales del Enrutamiento
- HTTP Nativo en Go: El Punto de Partida
- Arquitectura Hexagonal: Adaptadores de Comunicación
- REST: El Estándar de Facto
- gRPC: Cuando Necesitas Velocidad
- Más Allá de HTTP: WebSockets, STOMP y Más
- Patrones Profesionales y Mejores Prácticas
- Antipatrones: Lo Que NO Debes Hacer
- 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:
- Clara: Alguien debe poder leer tu código y entender inmediatamente qué sucede
- Rápida: Tienes milisegundos para procesar una request
- Confiable: Debe manejar miles de conexiones simultáneamente
- 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:
- Protocolo: HTTP, gRPC, WebSocket, etc
- Método: GET, POST, PUT, DELETE (en HTTP)
- Ruta: /users, /orders/123, /api/v1/transfer
- 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 JSONAccept: application/xml→ Devuelves XMLAccept: application/x-protobuf→ Devuelves Protocol Buffers
El mismo endpoint. Diferentes formatos. Esto se llama “content negotiation” y es poderoso porque:
- Tu dominio no cambia
- Tu lógica de negocio no cambia
- 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:
- Crea un listener TCP en puerto 8080
- Acepta conexiones (en goroutines automáticamente)
- Parsea HTTP de cada conexión
- Routea a handlers registrados
- 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:
- Controlable: Puedes arrancar/parar el servidor
- Configurable: Timeouts, etc.
- Testeable: Puedes crear múltiples instancias en tests
- 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:
- Recursos (nouns, no verbs):
/users,/orders,/transfers - Métodos HTTP correctos: GET (read), POST (create), PUT (replace), PATCH (update), DELETE
- Status codes significativos: 200, 201, 204, 400, 401, 403, 404, 409, 422, 500, etc.
- 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
- Net/HTTP stdlib: https://pkg.go.dev/net/http
- gRPC Go: https://grpc.io/docs/languages/go/
- Protocol Buffers: https://protobuf.dev/
- Gorilla WebSocket: https://github.com/gorilla/websocket
- Go Best Practices: https://golang.org/doc/effective_go
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.