Crear un Servidor Web con Golang

El servidor web de la biblioteca estándar de Go es de los pocos que se pueden poner en producción tal cual. Y desde Go 1.22 su enrutador entiende métodos y parámetros en la ruta, que era lo único por lo que hacía falta una biblioteca externa.

El servidor

package main

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

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

	mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hola desde Go")
	})
	mux.HandleFunc("GET /saludo/{nombre}", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hola, %s\n", r.PathValue("nombre"))
	})
	mux.HandleFunc("GET /api/estado", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]any{"ok": true, "version": 1})
	})

	srv := &http.Server{
		Addr:              ":8080",
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       10 * time.Second,
		WriteTimeout:      10 * time.Second,
	}
	log.Fatal(srv.ListenAndServe())
}
cinco peticionesGET  /            200 "Hola desde Go\n"
GET  /saludo/Ana  200 "Hola, Ana\n"
GET  /api/estado  200 "{\"ok\":true,\"version\":1}\n"
POST /api/estado  405 "Method Not Allowed\n"
GET  /nada        404 "404 page not found\n"

Las rutas

PatrónQué recibe
GET /{$}solo la raíz, exacta
GET /saludo/{nombre}/saludo/Ana; el valor, con r.PathValue("nombre")
GET /archivos/{ruta...}todo lo que venga detrás, barras incluidas
POST /api/usuariossolo POST; otro método recibe un 405

Fíjate en el {$} de la primera ruta. Sin él, "/" en Go significa «la raíz y todo lo que empiece por ella», es decir, cualquier ruta. Era el comportamiento del ejemplo anterior de esta página, que respondía «Saludos, bienvenido» a cualquier dirección que escribieras, incluida una que no existía. Con {$}, lo que no está definido recibe su 404.

Y el 405 lo pone Go solo: la ruta existe para GET, y si alguien hace un POST, la respuesta es «método no permitido», con la cabecera Allow indicando cuáles sí.

Los tiempos de espera

http.ListenAndServe(":8080", nil), el atajo de siempre, no tiene ningún tiempo de espera. Un cliente que abre la conexión y envía las cabeceras letra a letra, muy despacio, la mantiene ocupada indefinidamente; con unos cuantos así, el servidor se queda sin conexiones. Es un ataque conocido con nombre propio, Slowloris. Por eso el ejemplo construye un http.Server con ReadHeaderTimeout, ReadTimeout y WriteTimeout: para algo expuesto a internet no son opcionales.

Para una API de verdad faltarían dos piezas que también están en la biblioteca estándar: un apagado ordenado con srv.Shutdown(ctx), que deja terminar las peticiones en curso, y registrar cada petición. Pero ninguna exige añadir dependencias.

Más información en inglés: Create a web server with Golang.

Todo el código se compiló y se ejecutó con Go 1.27 en un contenedor limpio antes de publicar esta página; las salidas están copiadas de esa ejecución.