🌍 Servidores web con net/http
Handlers, ServeMux y rutas modernas, request y response, middleware, templates, archivos estáticos y un servidor de producción con timeouts y graceful shutdown.
Servidores web con net/http
El paquete net/http de la librería estándar es suficiente para construir una API REST de producción completa, sin frameworks. Su modelo es simple: todo es un Handler que recibe una http.ResponseWriter y un *http.Request. Aquí aprenderás desde el handler más básico hasta un servidor con middleware, templates y graceful shutdown.
El Handler básico
Un http.Handler es cualquier tipo con el método ServeHTTP. http.HandlerFunc es un adaptador para que una función simple sirva como handler.
package main
import (
"fmt"
"net/http"
)
func hola(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hola, mundo!")
}
func main() {
http.HandleFunc("/", hola)
http.ListenAndServe(":8080", nil)
}
http.HandleFuncregistra unahttp.HandlerFuncen el mux por defecto.http.ListenAndServeescucha en el puerto y sirve las rutas. Connilusa el mux global.
Forma explícita con tipos
type miHandler struct{}
func (miHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("soy un handler"))
}
func main() {
http.Handle("/handler", miHandler{})
http.ListenAndServe(":8080", nil)
}
💡 En la práctica casi siempre usas
http.HandlerFunc(una función) en lugar de definir un tipo. El mux moderno se encarga de todo.
ServeMux y rutas modernas (Go 1.22+)
http.ServeMux es el router de la estándar. Desde Go 1.22 soporta métodos HTTP y wildcards en la ruta:
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
// método + ruta exacta
mux.HandleFunc("GET /usuarios", listarUsuarios)
// wildcard {id} captura el segmento
mux.HandleFunc("GET /usuarios/{id}", obtenerUsuario)
// wildcard {nombre...} captura el resto de la ruta
mux.HandleFunc("/static/{archivo...}", servirArchivo)
// cualquier método
mux.HandleFunc("/health", health)
http.ListenAndServe(":8080", mux)
}
Para leer el wildcard se usa r.PathValue:
func obtenerUsuario(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "usuario %s", id)
}
⚠️ En Go < 1.22 el
ServeMuxsolo hacia prefijos y no distinguía métodos. Si ves código antiguo con rutas tipo"/usuarios"que responden a todo, es la versión anterior.
Request: método, query y path
El *http.Request concentra todo lo que llega:
func handler(w http.ResponseWriter, r *http.Request) {
// método HTTP
fmt.Println("Método:", r.Method)
// ruta (sin query)
fmt.Println("Path:", r.URL.Path)
// query string: /?edad=30&nombre=ana
q := r.URL.Query()
fmt.Println("nombre:", q.Get("nombre"))
fmt.Println("edad:", q.Get("edad"))
// encabezados
fmt.Println("User-Agent:", r.Header.Get("User-Agent"))
}
Request con body JSON
Para leer un JSON del cuerpo, decodifícalo directamente con encoding/json:
type Usuario struct {
Nombre string `json:"nombre"`
Edad int `json:"edad"`
}
func crearUsuario(w http.ResponseWriter, r *http.Request) {
var u Usuario
// decodifica el body JSON en el struct
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
http.Error(w, "json inválido", http.StatusBadRequest)
return
}
defer r.Body.Close()
fmt.Printf("recibido: %+v\n", u)
w.WriteHeader(http.StatusCreated)
}
💡 Los tags
json:"nombre"controlan el nombre en el JSON. Sin tag, se usa el nombre del campo (con mayúscula).
Response: encabezados, status y JSON
Escribir una respuesta implica fijar encabezados, status code y cuerpo:
func responderJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func obtenerPerfil(w http.ResponseWriter, r *http.Request) {
perfil := map[string]any{"nombre": "ana", "admin": true}
responderJSON(w, http.StatusOK, perfil)
}
Status codes habituales: http.StatusOK (200), Created (201), NoContent (204), BadRequest (400), Unauthorized (401), NotFound (404), InternalServerError (500).
⚠️ Si escribes el cuerpo con
w.Writeantes deWriteHeader, el status se fija implícitamente a 200. Fija siempre elContent-Typey el status antes de escribir el body.
Middleware
Un middleware envuelve un handler para añadir comportamiento antes o después de que se procese la petición. El patrón es una función que recibe un handler y devuelve otro.
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
inicio := time.Now()
next.ServeHTTP(w, r) // delega
fmt.Printf("%s %s en %v\n", r.Method, r.URL.Path, time.Since(inicio))
})
}
Encadenar middleware
func recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
http.Error(w, "error interno", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
func cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /api", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
})
// el orden importa: se ejecutan de fuera hacia dentro
handler := logging(cors(recovery(mux)))
http.ListenAndServe(":8080", handler)
}
💡 El orden de anidación determina el flujo: lo más externo se ejecuta primero (logging ve la petición antes de cors, etc.). Para autenticación, ponla en el exterior para que rechace antes de llegar al negocio.
Templates con html/template
html/template genera HTML y escapa automáticamente todo lo dinámico, previniendo XSS. No uses fmt para generar HTML.
import "html/template"
var tpl = template.Must(template.ParseFiles("templates/index.html"))
func pagina(w http.ResponseWriter, r *http.Request) {
data := map[string]any{"Titulo": "Bienvenido", "Nombre": "<script>alert('x')</script>"}
tpl.Execute(w, data) // Nombre sale escapado, inofensivo
}
templates/index.html:
<!DOCTYPE html>
<html>
<body>
<h1>{{.Titulo}}</h1>
<p>Hola, {{.Nombre}}</p>
</body>
</html>
💡
template.Mustcompila la plantilla al arrancar y lanza panic si hay error: falla rápido en desarrollo. El escape automático dehtml/templatehace el{{.Nombre}}seguro aunque contenga HTML.
Archivos estáticos
http.FileServer sirve archivos de un directorio:
func main() {
mux := http.NewServeMux()
// sirve /static/... desde la carpeta public/
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public"))))
http.ListenAndServe(":8080", mux)
}
http.Dir("public")— lee del directorio.http.StripPrefix— quita el prefijo de la URL para que coincida con el árbol de archivos.- El
FileServerfijaContent-Typesegún la extensión y gestiona caché.
⚠️ Nunca sirvas la carpeta raíz del proyecto como estática: expondrías el código fuente y los
.gofiles. Usa una carpeta dedicada (public/,assets/).
Servidor de producción: timeouts y graceful shutdown
http.ListenAndServe es cómodo para desarrollo, pero en producción configuras http.Server con timeouts y cierre elegante:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
})
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
// graceful shutdown: espera a las peticiones en curso
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Println("error servidor:", err)
}
}()
<-ctx.Done() // espera señal (Ctrl+C, SIGTERM)
fmt.Println("apagando…")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
fmt.Println("shutdown forzado:", err)
}
fmt.Println("servidor detenido")
}
💡 Configura siempre
ReadHeaderTimeoutpara evitar ataques de slowloris.signal.NotifyContext+srv.Shutdownpermiten terminar limpiamente: espera a las peticiones activas antes de cerrar.
Estructurar una app web real: handlers / services / storage
Para que crezca sin convertirse en un monolito caótico, separa en capas:
webapp/
├── cmd/server/main.go # arranque, wiring, rutas
├── internal/
│ ├── httpapi/ # handlers y rutas
│ ├── service/ # lógica de negocio
│ └── storage/ # acceso a datos
El flujo típico: handler valida y decodifica → service aplica lógica → storage persiste. Cada capa depende de la anterior por interfaces.
API REST pequeña completa
Juntando todo: un CRUD mínimo con rutas modernas, JSON y middleware:
package main
import (
"encoding/json"
"net/http"
"strconv"
"time"
)
type Tarea struct {
ID int `json:"id"`
Titulo string `json:"titulo"`
Hecha bool `json:"hecha"`
}
var tareas = []Tarea{{ID: 1, Titulo: "aprender Go"}}
var siguienteID = 2
func listar(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(tareas)
}
func crear(w http.ResponseWriter, r *http.Request) {
var t Tarea
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
http.Error(w, "json inválido", http.StatusBadRequest)
return
}
t.ID = siguienteID
siguienteID++
tareas = append(tareas, t)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(t)
}
func obtener(w http.ResponseWriter, r *http.Request) {
id, _ := strconv.Atoi(r.PathValue("id"))
for _, t := range tareas {
if t.ID == id {
json.NewEncoder(w).Encode(t)
return
}
}
http.Error(w, "no encontrado", http.StatusNotFound)
}
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
inicio := time.Now()
next.ServeHTTP(w, r)
httpReq := r
_ = httpReq
println(r.Method, r.URL.Path, time.Since(inicio).String())
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /tareas", listar)
mux.HandleFunc("POST /tareas", crear)
mux.HandleFunc("GET /tareas/{id}", obtener)
srv := &http.Server{
Addr: ":8080",
Handler: logging(mux),
ReadHeaderTimeout: 5 * time.Second,
}
println("escuchando en :8080")
srv.ListenAndServe()
}
Cheatsheet
| Necesitas… | Usa… |
|---|---|
| Registrar ruta | mux.HandleFunc("GET /ruta", fn) |
| Capturar segmento | r.PathValue("id") |
| Leer query | r.URL.Query().Get("k") |
| Decodificar JSON | json.NewDecoder(r.Body).Decode(&v) |
| Responder JSON | json.NewEncoder(w).Encode(v) |
| Envolver handler | middleware: fn(http.Handler) http.Handler |
| HTML con escape | html/template |
| Archivos estáticos | http.FileServer(http.Dir("public")) |
| Servidor robusto | http.Server + timeouts |
| Cierre elegante | signal.NotifyContext + srv.Shutdown |
Para profundizar
- net/http — pkg.go.dev: referencia completa del paquete.
- Writing Web Applications: tutorial oficial de una wiki en Go.
- Go by Example — HTTP Servers: ejemplos cortos y claros.
- html/template — pkg.go.dev: escape automático y plantillas.
- The Go Programming Language: capítulos de servidores y networking.