En el núcleo de cualquier servidor web en Go reside la interfaz http.Handler. Esta interfaz define un contrato único: cualquier tipo que implemente el método ServeHTTP(http.ResponseWriter, *http.Request) es, por definición, un manejador capaz de procesar una petición HTTP. Para que el sistema sea flexible, Go utiliza este modelo de interfaz en lugar de depender de tipos concretos, lo que permite que tanto estructuras complejas con estado como funciones simples puedan actuar como puntos de entrada de la lógica de negocio.
Sin embargo, implementar un método en un struct para cada endpoint resultaría tedioso. Por eso existe http.HandlerFunc, que es un tipo de función con un método ServeHTTP implementado [disponible desde Go 1.0]. Este es un adaptador que permite convertir una función con la firma func(http.ResponseWriter, *http.Request) en un http.Handler. Esta distinción es la que permite el patrón de middleware: un middleware no envuelve a una función, sino que envuelve a un http.Handler para interceptar la ejecución, permitiéndonos encadenar comportamientos (logging, autenticación, recuperación de pánicos) de forma recursiva.
Cuando implementes la lógica de respuesta, debes tratar a http.ResponseWriter como la interfaz que es, no como un struct. Esta interfaz te da control sobre tres operaciones: Header() para manipular las cabeceras, WriteHeader(int) para fijar el código de estado, y Write([]byte) para enviar el cuerpo. El orden de estas llamadas es crítico: las cabeceras deben enviarse antes que el cuerpo. Si llamas a Write(), Go enviará automáticamente un 200 OK y las cabeceras actuales al cliente; una vez que se ha enviado el primer byte del cuerpo, ya no puedes cambiar el código de estado ni las cabeceras.
¿Cuándo usar cada uno? Utiliza un struct que implemente http.Handler cuando tu manejador necesite estado persistente o dependencias inyectadas (como una conexión a una base de datos o un cliente de Redis). Usa http.HandlerFunc cuando la lógica sea puramente funcional o cuando estés construyendo middlewares que deban envolver otros handlers. Si te equivocas en el orden de WriteHeader o intentas modificar cabeceras después de haber escrito datos, no obtendrás un error de compilación, sino un comportamiento errático en el cliente o cabeceras ignoradas que son extremadamente difíciles de depurar en producción.
package main
import (
"fmt"
"net/http"
"time"
)
// LoggingMiddleware es un decorador que implementa el patrón de envoltura.
// Recibe un http.Handler y devuelve otro http.Handler.
func LoggingMiddleware(next http.Handler) http.Handler {
// Usamos http.HandlerFunc para convertir esta función en un http.Handler
// de forma inmediata y elegante.
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Ejecutamos el siguiente handler en la cadena
next.ServeHTTP(w, r)
fmt.Printf("LOG: %s %s %s\n", r.Method, r.URL.Path, time.Since(start))
})
}
// UserHandler es un struct que implementa la interfaz http.Handler.
// Se usa aquí para demostrar la inyección de dependencias.
type UserHandler struct {
DBName string
}
func (h *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Primero manipulamos las cabeceras
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-DB-Source", h.DBName)
// Luego definimos el código de estado
w.WriteHeader(http.StatusOK)
// Finalmente escribimos el cuerpo de la respuesta
w.Write([]byte(`{"status": "ok", "user": "gopher"}`))
}
func main() {
// Instanciamos el handler con su estado/dependencia
userHandler := &UserHandler{
DBName: "production_db",
}
// Envolvemos el handler con nuestro middleware de logging
// Fíjate que el middleware devuelve un http.Handler
finalHandler := LoggingMiddleware(userHandler)
// El servidor HTTP de Go acepta un http.Handler como punto de entrada
fmt.Println("Servidor corriendo en :8080...")
if err := http.ListenAndServe(":8080", finalHandler); err != nil {
panic(err)
}
}
Desglose del código
En el ejemplo, LoggingMiddleware es la pieza clave de la arquitectura de middleware. No devuelve una función simple, sino que devuelve un http.Handler. Para lograrlo sin definir un nuevo struct, utilizamos http.HandlerFunc(func(...) { ... }). Esto es posible porque http.HandlerFunc tiene un método ServeHTTP definido sobre el tipo de la función misma, permitiendo que la función “se convierta” en un objeto que satisface la interfaz.
El UserHandler ilustra cómo manejar la inyección de dependencias en un servidor de alto rendimiento. Al ser un struct, puede mantener una conexión a la base de datos o configuraciones de entorno (DBName) de forma segura sin depender de variables globales. Dentro de su método ServeHTTP, observamos la secuencia obligatoria: Header().Set(...) $\rightarrow$ WriteHeader(...) $\rightarrow$ Write(...). Si moviéramos w.WriteHeader(http.StatusOK) para después de w.Write(...), la llamada a WriteHeader sería totalmente ignorada por el runtime, ya que el encabezado HTTP ya habría sido enviado al socket en el momento de ejecutar el Write.
Al final, http.ListenAndServe recibe finalHandler, que es el resultado de haber envuelto el UserHandler con el middleware. Cada petición atraviesa primero la lógica de LoggingMiddleware (el cronómetro) y luego delega la ejecución real en UserHandler.ServeHTTP mediante la llamada next.ServeHTTP(w, r).
El error frecuente
Un error clásico ocurre cuando intentas cambiar el código de estado después de haber escrito algo en el cuerpo de la respuesta:
func (h *BadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Algo de información")) // Aquí Go envía implícitamente 200 OK
// Esto es un error silencioso: el cliente ya recibió el 200 OK
// y el encabezado Content-Length. Esta llamada no hará nada.
w.WriteHeader(http.StatusInternalServerError)
}
En este escenario, el cliente recibirá un 200 OK con el mensaje, a pesar de que tu intención lógica era reportar un error 500. Siempre debes asegurar que el flujo de ejecución no toque Write hasta que todas las decisiones sobre el código de estado y las cabeceras estén tomadas.
N° 189