En esta página

HTTP y APIs REST

14 min lectura TextoCap. 5 — Go en producción

HTTP y APIs REST en Go: la biblioteca estándar es suficiente

Una de las características más sorprendentes de Go es que su biblioteca estándar incluye un servidor HTTP completo, production-grade. Empresas como Google, Cloudflare y Dropbox han construido sistemas que manejan millones de requests por segundo usando net/http sin frameworks externos.

Go 1.22 mejoró significativamente el ServeMux estándar con soporte para método HTTP en el pattern y path parameters, eliminando la necesidad de routers de terceros para la mayoría de casos de uso.

El servidor HTTP más simple

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "¡Hola, Go HTTP!")
    })

    // ListenAndServe bloquea hasta que el servidor se detiene
    http.ListenAndServe(":8080", nil)  // usa el DefaultServeMux global
}

`http.Request`: accediendo a los datos del request

func handler(w http.ResponseWriter, r *http.Request) {
    // Método HTTP
    fmt.Println("Método:", r.Method)  // GET, POST, PUT, DELETE, etc.

    // URL y path
    fmt.Println("URL:", r.URL)
    fmt.Println("Path:", r.URL.Path)
    fmt.Println("Query:", r.URL.RawQuery)

    // Query parameters
    q := r.URL.Query()
    nombre := q.Get("nombre")         // ?nombre=Ana
    página := q.Get("página")         // ?página=2
    _ = nombre
    _ = página

    // Headers
    token := r.Header.Get("Authorization")
    contentType := r.Header.Get("Content-Type")
    _ = token
    _ = contentType

    // Body (para POST/PUT)
    defer r.Body.Close()
    // Leer directamente
    datos, err := io.ReadAll(r.Body)
    _ = err
    _ = datos
}

`http.ResponseWriter`: escribir respuestas

func handler(w http.ResponseWriter, r *http.Request) {
    // Headers deben escribirse ANTES del status y el body
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Custom-Header", "valor")

    // Status code (solo se puede escribir una vez)
    w.WriteHeader(http.StatusCreated)  // 201

    // Body
    w.Write([]byte(`{"mensaje": "creado"}`))

    // Helpers convenientes de http
    http.Error(w, "No encontrado", http.StatusNotFound)
    http.Redirect(w, r, "/nueva-ruta", http.StatusMovedPermanently)
}

JSON: codificación y decodificación

import "encoding/json"

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

// Codificar struct → JSON
func escribirJSON(w http.ResponseWriter, status int, v any) error {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(status)
    return json.NewEncoder(w).Encode(v)
}

// Decodificar JSON → struct
func leerJSON(r *http.Request, v any) error {
    defer r.Body.Close()
    // Limitar tamaño del body a 1MB
    r.Body = http.MaxBytesReader(nil, r.Body, 1<<20)
    decoder := json.NewDecoder(r.Body)
    decoder.DisallowUnknownFields()  // rechazar campos desconocidos
    return decoder.Decode(v)
}

ServeMux de Go 1.22+: pattern matching avanzado

mux := http.NewServeMux()

// Patrón con método HTTP (nuevo en Go 1.22)
mux.HandleFunc("GET /usuarios", listarUsuarios)
mux.HandleFunc("POST /usuarios", crearUsuario)
mux.HandleFunc("GET /usuarios/{id}", obtenerUsuario)
mux.HandleFunc("PUT /usuarios/{id}", actualizarUsuario)
mux.HandleFunc("DELETE /usuarios/{id}", eliminarUsuario)

// Acceder a path parameters
func obtenerUsuario(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")  // r.PathValue disponible desde Go 1.22
    // ...
}

// Subtree routing (el / al final captura cualquier subruta)
mux.HandleFunc("/static/", servirArchivosEstáticos)

// Host-specific routing
mux.HandleFunc("api.mi-app.com/v1/", apiV1Handler)

Middleware: funciones que envuelven handlers

El patrón de middleware en Go es simple: una función que recibe un http.Handler y retorna otro http.Handler:

// Tipo de middleware
type Middleware func(http.Handler) http.Handler

// Middleware de autenticación
func autenticar(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "No autorizado", http.StatusUnauthorized)
            return
        }
        // Verificar token...
        next.ServeHTTP(w, r)
    })
}

// Middleware de CORS
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, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }

        next.ServeHTTP(w, r)
    })
}

// Middleware de logging con slog (Go 1.21+)
func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        inicio := time.Now()
        rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
        next.ServeHTTP(rw, r)
        slog.Info("http",
            "método", r.Method,
            "ruta", r.URL.Path,
            "status", rw.status,
            "duración", time.Since(inicio),
        )
    })
}

type responseWriter struct {
    http.ResponseWriter
    status int
}

func (rw *responseWriter) WriteHeader(status int) {
    rw.status = status
    rw.ResponseWriter.WriteHeader(status)
}

// Encadenar middlewares
func encadenar(h http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

API REST completa: estructura de un proyecto real

// handlers/usuario.go
type UsuarioHandler struct {
    repo *UsuarioRepositorio
    log  *slog.Logger
}

func (h *UsuarioHandler) Listar(w http.ResponseWriter, r *http.Request) {
    usuarios, err := h.repo.Todos(r.Context())
    if err != nil {
        h.log.Error("error listando usuarios", "error", err)
        writeError(w, http.StatusInternalServerError, "error interno")
        return
    }
    writeJSON(w, http.StatusOK, usuarios)
}

func (h *UsuarioHandler) Crear(w http.ResponseWriter, r *http.Request) {
    var req struct {
        Nombre string `json:"nombre"`
        Email  string `json:"email"`
    }

    if err := leerJSON(r, &req); err != nil {
        writeError(w, http.StatusBadRequest, "JSON inválido: "+err.Error())
        return
    }

    if req.Nombre == "" || req.Email == "" {
        writeError(w, http.StatusUnprocessableEntity, "nombre y email son requeridos")
        return
    }

    usuario, err := h.repo.Crear(r.Context(), req.Nombre, req.Email)
    if err != nil {
        writeError(w, http.StatusInternalServerError, "error al crear usuario")
        return
    }

    writeJSON(w, http.StatusCreated, usuario)
}

// main.go
func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    repo := NewUsuarioRepositorio()
    handler := &UsuarioHandler{repo: repo, log: logger}

    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/v1/usuarios", handler.Listar)
    mux.HandleFunc("POST /api/v1/usuarios", handler.Crear)
    mux.HandleFunc("GET /api/v1/usuarios/{id}", handler.ObtenerPorID)

    srv := &http.Server{
        Addr:         ":8080",
        Handler:      encadenar(mux, logging, cors, recuperarPanico),
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 30 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    logger.Info("Servidor iniciado", "addr", srv.Addr)
    if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
        logger.Error("error del servidor", "error", err)
        os.Exit(1)
    }
}

Sirviendo archivos estáticos

// Servir archivos desde un directorio
mux.Handle("GET /static/", http.StripPrefix("/static/",
    http.FileServer(http.Dir("./public"))))

// Usando embed para archivos incrustados en el binario
import "embed"

//go:embed public
var archivosEstáticos embed.FS

mux.Handle("GET /static/", http.FileServerFS(archivosEstáticos))

Con net/http dominado, estás listo para el proyecto final: construirás un gestor de tareas CLI con API HTTP que aplica todo lo aprendido en el curso.

Go 1.22 mejoró radicalmente el ServeMux
Antes de Go 1.22, el ServeMux estándar no soportaba pattern matching con métodos HTTP ni path parameters. Go 1.22 introdujo patrones como GET /users/{id} con r.PathValue("id"). Para la mayoría de APIs REST, ya no necesitas routers externos como Gorilla Mux o Chi.
Siempre configura timeouts en http.Server
Un http.Server sin timeouts es vulnerable a ataques de slowloris y puede agotar conexiones en producción. Configura siempre ReadTimeout, WriteTimeout e idealmente IdleTimeout. Para APIs, valores de 10-30 segundos son razonables. Para uploads grandes, usa ReadHeaderTimeout separado.