servidor-http-basico-go-net-http

Servidor HTTP básico en Go con net/http, rutas y JSON

  • 3 min

Un servidor HTTP en Go es un programa que escucha peticiones web y devuelve respuestas usando normalmente el paquete estándar net/http.

Con la biblioteca estándar podemos levantar un servidor, registrar rutas y devolver texto o JSON sin instalar dependencias externas.

Vamos a crear el servidor más pequeño posible y luego lo iremos haciendo un poquito más útil.

El servidor mínimo

Empezamos con lo básico:

package main

import (
    "fmt"
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "Hola desde Go")
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}
Copied!

Ejecutamos:

go run main.go
Copied!

Y abrimos en el navegador:

http://localhost:8080
Copied!

http.HandleFunc registra una función para una ruta. http.ResponseWriter sirve para escribir la respuesta. *http.Request contiene la información de la petición. ListenAndServe arranca el servidor en el puerto indicado.

Handlers con nombre

Para ejemplos pequeños, una función anónima está bien. En cuanto el código crece un poco, conviene separar los handlers.

package main

import (
    "fmt"
    "log"
    "net/http"
)

func inicio(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Página de inicio")
}

func salud(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "OK")
}

func main() {
    http.HandleFunc("/", inicio)
    http.HandleFunc("/health", salud)

    log.Fatal(http.ListenAndServe(":8080", nil))
}
Copied!

Esto ya se parece más a una aplicación real: cada ruta tiene su responsabilidad.

Leer método y ruta

El objeto *http.Request tiene mucha información sobre la petición.

func info(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Método: %s\n", r.Method)
    fmt.Fprintf(w, "Ruta: %s\n", r.URL.Path)
}
Copied!

Desde Go 1.22, ServeMux puede incluir el método en el patrón y devuelve automáticamente 405 Method Not Allowed con la cabecera Allow cuando la ruta existe para otros métodos:

func crearUsuario(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Usuario creado")
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("POST /usuarios", crearUsuario)
    log.Fatal(http.ListenAndServe(":8080", mux))
}
Copied!

Cuando respondáis un error manualmente, usad return después si no queréis que el handler siga ejecutándose.

Parámetros de ruta

Los patrones también admiten comodines. Leemos su valor con PathValue:

mux.HandleFunc("GET /usuarios/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    fmt.Fprintln(w, "Usuario:", id)
})
Copied!

Un patrón / coincide con cualquier ruta no capturada por otro patrón. Para representar solo la raíz podemos usar GET /{$}.

Devolver JSON

Un servidor moderno casi siempre acaba devolviendo JSON.

package main

import (
    "encoding/json"
    "log"
    "net/http"
)

type Respuesta struct {
    Mensaje string `json:"mensaje"`
    Estado  string `json:"estado"`
}

func api(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")

    respuesta := Respuesta{
        Mensaje: "Hola desde Go",
        Estado:  "ok",
    }

    if err := json.NewEncoder(w).Encode(respuesta); err != nil {
        log.Printf("codificar respuesta: %v", err)
    }
}
Copied!

Usamos json.NewEncoder(w) porque ResponseWriter ya es un destino de escritura. No hace falta montar el JSON en memoria antes.

Usar un ServeMux propio

En los ejemplos rápidos se suele usar el mux global con http.HandleFunc. Para aplicaciones reales, es mejor crear uno propio.

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

    mux.HandleFunc("GET /{$}", inicio)
    mux.HandleFunc("GET /api", api)

    log.Fatal(http.ListenAndServe(":8080", mux))
}
Copied!

Así evitamos depender de estado global y el código queda más fácil de probar y mantener.

Configurar el servidor y controlar errores

ListenAndServe devuelve un error. Para un servicio real también conviene configurar límites de tiempo, en especial ReadHeaderTimeout:

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

    servidor := &http.Server{
        Addr:              ":8080",
        Handler:           mux,
        ReadHeaderTimeout: 5 * time.Second,
        IdleTimeout:       60 * time.Second,
    }

    if err := servidor.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
        log.Fatal(err)
    }
}
Copied!

http.ErrServerClosed es el resultado normal cuando hacemos un apagado ordenado con Shutdown. Los demás errores indican que el servidor no pudo arrancar o dejó de servir inesperadamente.