🔎 Buscar

🌍 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.

Wiki / Apuntes📖 Contenido

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.HandleFunc registra una http.HandlerFunc en el mux por defecto.
  • http.ListenAndServe escucha en el puerto y sirve las rutas. Con nil usa 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 ServeMux solo 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.Write antes de WriteHeader, el status se fija implícitamente a 200. Fija siempre el Content-Type y 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.Must compila la plantilla al arrancar y lanza panic si hay error: falla rápido en desarrollo. El escape automático de html/template hace 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 FileServer fija Content-Type segú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 .go files. 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 ReadHeaderTimeout para evitar ataques de slowloris. signal.NotifyContext + srv.Shutdown permiten 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

Estudio · Recursos de todo el mundo (inglés, chino, japonés, español, francés, ruso…) curados y traducidos al español.