La decisión de utilizar la biblioteca estándar net/http o un framework externo como chi, echo o gin no es una cuestión de preferencia estética, sino de gestión de complejidad y control de dependencias. La biblioteca estándar es el núcleo sobre el cual se construye todo el ecosistema; es extremadamente estable y su API es predecible. Sin embargo, los frameworks introducen capas de abstracción para resolver problemas de ergonomía que la stdlib no aborda de forma nativa, como el binding de JSON, la validación de esquemas o el manejo de sub-rutas complejas.
Desde la llegada de Go 1.22 [disponible desde Go 1.22], el http.ServeMux estándar se ha vuelto mucho más capaz gracias al soporte nativo de métodos (GET, POST, etc.) y patrones de rutas con variables (como /user/{id}). Esto ha eliminado la necesidad de usar frameworks solo para el enrutamiento básico.
La lógica interna es simple: un framework añade un “wrapper” sobre http.Handler. Si usas un framework muy opinativo como gin, estarás trabajando con una firma de función distinta a la estándar, lo que te obliga a escribir adaptadores para usar middlewares de la comunidad. Si usas chi, te mantienes dentro de la filosofía de la stdlib pero con un motor de enrutamiento más potente.
Debes optar por la stdlib cuando construyas microservicios de baja latencia, APIs internas muy simples o cuando la estabilidad a largo plazo sea más crítica que la velocidad de desarrollo. Un framework es la opción correcta cuando tu API tiene una jerarquía compleja de rutas, necesitas grupos de middleware específicos para distintas secciones de la URL, o quieres evitar el boilerplate de parsear manualmente cuerpos de request en JSON.
Si eliges mal, el error no suele ser de rendimiento (los frameworks modernos son extremadamente rápidos), sino de arquitectura: usar un framework pesado para una función Lambda que solo responde un “OK” es añadir complejidad innecesaria; o intentar construir tu propio sistema de sub-rutas y middleware con net/http puro cuando el proyecto escala, lo que termina derivando en un código propietario, propenso a errores y difícil de mantener.
package main
import (
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
// User representa nuestro modelo de datos.
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
// LoggerMiddleware es un middleware personalizado que simula un log de auditoría.
// Nota cómo mantiene la firma estándar de http.Handler.
func LoggerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r) // Ejecuta el siguiente handler en la cadena
fmt.Printf("[%s] %s %s %v\n", time.Now().Format(time.RFC3339), r.Method, r.URL.Path, time.Since(start))
})
}
func main() {
// Usamos chi porque es un "idiomatic router".
// Es un framework que no intenta reinventar el estándar, sino complementarlo.
r := chi.NewRouter()
// 1. Middleware global: Se aplica a todas las rutas.
r.Use(middleware.Recoverer) // Protege contra panics que tirarían el proceso.
r.Use(LoggerMiddleware)
// 2. Rutas de nivel superior (Simples)
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("UP"))
})
// 3. Routing con grupos y sub-rutas (Aquí es donde la stdlib se vuelve verbosa)
// Creamos un grupo de la API para aplicar middleware de autenticación solo aquí.
r.Route("/api/v1", func(r chi.Router) {
// Aquí podríamos añadir r.Use(AuthMiddleware)
r.Get("/status", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "active"})
})
// Sub-ruta para recursos específicos (users)
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers) // GET /api/v1/users
r.Get("/{userID}", getUser) // GET /api/v1/users/123
r.Post("/", createUser) // POST /api/v1/users
})
})
fmt.Println("Servidor corriendo en :8080")
if err := http.ListenAndServe(":8080", r); err != nil {
panic(err)
}
}
func listUsers(w http.ResponseWriter, r *http.Request) {
users := []User{{ID: "1", Name: "Alice", Email: "alice@example.com"}}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func getUser(w http.ResponseWriter, r *http.Request) {
// chi.URLParam extrae parámetros de la ruta de forma eficiente.
userID := chi.URLParam(r, "userID")
user := User{ID: userID, Name: "Bob", Email: "bob@example.com"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func createUser(w http.ResponseWriter, r *http.Request) {
var u User
// En un framework como Gin o Echo, esto sería un solo llamado: c.BindJSON(&u)
// Aquí, con chi/stdlib, lo hacemos manualmente para mantener el control.
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
http.Error(w, "Invalid payload", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(u)
}
Análisis del ejemplo
El código utiliza chi para demostrar cómo gestionar una jerarquía de rutas sin perder la compatibilidad con el ecosistema de Go.
En el bloque de main, la función r.Route("/api/v1", ...) es la clave. En la stdlib de Go, para lograr ese nivel de anidación con middleware específicos para una rama de la URL, tendrías que crear manualmente múltiples http.ServeMux y conectarlos, lo cual se vuelve inmanejable rápidamente. chi permite definir “sub-routers” de forma declarativa.
Observa LoggerMiddleware. A diferencia de frameworks como gin, que utilizan un objeto de contexto propio (*gin.Context), nuestro middleware utiliza http.HandlerFunc. Esto significa que cualquier middleware que escribas para este proyecto funcionará no solo con chi, sino con cualquier router de Go, ya sea la stdlib o echo. Esto es lo que llamamos “diseño basado en interfaces”.
La función getUser utiliza chi.URLParam(r, "userID"). Internamente, chi utiliza el context del http.Request para inyectar estos parámetros. Esto es mucho más limpio y seguro que parsear la URL manualmente mediante strings o expresiones regulares, que es lo que solía ocurrir en versiones antiguas de Go.
Finalmente, la gestión de JSON en createUser muestra el “costo” de no usar un framework completo: tenemos que lidiar con json.NewDecoder y manejar explícitamente el http.Error. Un framework como echo abstraería esto para que solo te ocupes de la lógica, pero a cambio, tu código estaría acoplado a su tipo de contexto y a sus dependencias.
El error frecuente
Un error clásico al implementar middlewares personalizados como LoggerMiddleware es olvidar llamar a next.ServeHTTP(w, r).
// ERROR: Este middleware "se come" la petición
func BrokenMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Algo pasó")
// Falta: next.ServeHTTP(w, r)
// La petición termina aquí y el cliente recibe un 200 OK vacío.
})
}
Si no llamas a next.ServeHTTP, la cadena de ejecución se rompe. El servidor responderá al cliente (probablemente con un status 200 por defecto si no has escrito nada más), pero el handler real nunca se ejecutará. En entornos de producción, esto es extremadamente difícil de depurar porque no verás errores en los logs, simplemente notarás que tus endpoints “no hacen nada”.
N° 194