net/http en Go: construir un servidor HTTP con handlers, middleware y routing

Go incluye en la biblioteca estándar todo lo necesario para construir servidores HTTP de producción sin dependencias externas. El paquete net/http cubre desde un servidor básico de tres líneas hasta configuraciones con middleware, routing avanzado y graceful shutdown.

El servidor más sencillo

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hola, %s!", r.URL.Path[1:])
    })
    http.ListenAndServe(":8080", nil)
}

Handlers como tipos propios

Para estructuras más complejas, implementa la interfaz http.Handler con el método ServeHTTP. Esto permite inyectar dependencias como loggers o conexiones a bases de datos:

type servidor struct {
    db     *sql.DB
    logger *log.Logger
}

func (s *servidor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    switch r.URL.Path {
    case "/usuarios":
        s.listarUsuarios(w, r)
    case "/salud":
        w.WriteHeader(http.StatusOK)
        fmt.Fprintln(w, "OK")
    default:
        http.NotFound(w, r)
    }
}

func (s *servidor) listarUsuarios(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    // consultar s.db y escribir JSON...
    fmt.Fprintln(w, `[{"id":1,"nombre":"Ana"}]`)
}

ServeMux para routing

Desde Go 1.22, el http.ServeMux soporta patrones con método HTTP y parámetros en la ruta:

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("GET /usuarios", listarUsuarios)
    mux.HandleFunc("POST /usuarios", crearUsuario)
    mux.HandleFunc("GET /usuarios/{id}", obtenerUsuario)
    mux.HandleFunc("DELETE /usuarios/{id}", borrarUsuario)

    http.ListenAndServe(":8080", mux)
}

func obtenerUsuario(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id") // nuevo en Go 1.22
    fmt.Fprintf(w, "usuario %s", id)
}

Middleware: encadenar comportamiento

Un middleware en Go es una función que recibe un Handler y devuelve otro. Se encadenan envolviéndolos uno dentro de 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)
        log.Printf("%s %s en %v", r.Method, r.URL.Path, time.Since(inicio))
    })
}

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 /", handler)

    // Encadenar: cors ? logging ? mux
    handler := cors(logging(mux))
    http.ListenAndServe(":8080", handler)
}

Respuestas JSON

json.NewEncoder escribe directamente al ResponseWriter sin crear un buffer intermedio:

type Usuario struct {
    ID     int    `json:"id"`
    Nombre string `json:"nombre"`
    Email  string `json:"email,omitempty"`
}

func responderJSON(w http.ResponseWriter, status int, datos any) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(datos)
}

func listarUsuarios(w http.ResponseWriter, r *http.Request) {
    usuarios := []Usuario{
        {ID: 1, Nombre: "Ana"},
        {ID: 2, Nombre: "Carlos", Email: "[email protected]"},
    }
    responderJSON(w, http.StatusOK, usuarios)
}

Ficheros estáticos

mux.Handle("GET /static/", http.StripPrefix("/static/",
    http.FileServer(http.Dir("./assets"))))

Graceful shutdown

Con http.Server configurado manualmente puedes detener el servidor esperando a que las peticiones en curso terminen antes de salir:

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /", handler)

    srv := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    go func() {
        if err := srv.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatalf("servidor: %v", err)
        }
    }()

    // Esperar señal del sistema operativo
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit

    ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
    defer cancel()

    if err := srv.Shutdown(ctx); err != nil {
        log.Fatal("shutdown forzado:", err)
    }
    log.Println("servidor detenido correctamente")
}

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP