La biblioteca estándar de Go trae un servidor HTTP listo para producción. Aprende handlers, enrutamiento con patrones de método y ruta en ServeMux, las reglas de ResponseWriter, qué pasa con cada petición y por qué http.Server necesita timeouts.
Go no necesita un framework web para servir HTTP. El paquete net/http de la biblioteca estándar tiene un servidor de verdad, un router y un cliente, y hay grandes servicios en producción que corren directamente sobre él.
Este post cubre los handlers, el enrutamiento con http.ServeMux, las reglas para escribir una respuesta, qué le pasa a una petición de principio a fin, y por qué conviene que construyas tú mismo un http.Server en lugar de llamar a http.ListenAndServe. Cada programa de abajo se ejecutó en Go 1.26, y su salida está copiada de esa ejecución.
Un handler es un solo método
En Go, un handler es cualquier valor con un método ServeHTTP. El paquete net/http lo define como una interfaz:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Es la misma idea de la parte sobre interfaces: una interfaz pequeña que se satisface sin declararlo. El servidor llama a ServeHTTP una vez por cada petición. El *http.Request contiene lo que envió el cliente. El http.ResponseWriter es donde escribes la respuesta.
Aquí hay un handler hecho a partir de un struct. Para correr un servidor real dentro de un ejemplo verificado, usamos httptest.NewServer. Arranca tu handler en un puerto libre al azar de tu propia computadora, te da su dirección en srv.URL y lo apaga cuando llamas a Close. Del lado del cliente está http.Get, que envía una petición real por una conexión real.
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
)
type greeter struct {
greeting string
}
func (g greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s, you asked for %s\n", g.greeting, r.URL.Path)
}
func main() {
var h http.Handler = greeter{greeting: "Hello"}
srv := httptest.NewServer(h)
defer srv.Close()
res, err := http.Get(srv.URL + "/tasks")
if err != nil {
fmt.Println("error:", err)
return
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(res.Status)
fmt.Println(res.Header.Get("Content-Type"))
fmt.Print(string(body))
}
Imprime:
200 OK
text/plain; charset=utf-8
Hello, you asked for /tasks
greeter nunca dice que implementa http.Handler. Tiene el método, así que la asignación compila. fmt.Fprintf funciona sobre w porque un ResponseWriter también es un io.Writer, la interfaz que conociste con io.Reader e io.Writer.
El handler nunca fijó un código de estado ni un Content-Type, y aun así el cliente recibió los dos. Cuando tú no eliges, el servidor envía 200 y adivina el tipo de contenido a partir de los primeros bytes que escribes. Más abajo vas a ver exactamente cuándo pasa eso.
http.HandlerFunc convierte una función en un handler
La mayoría de los handlers no necesitan un struct, así que net/http tiene un adaptador. http.HandlerFunc es un tipo función con un método ServeHTTP que simplemente llama a la función:
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { f(w, r) }
Convertir una función común a ese tipo le da el método, así que se vuelve un http.Handler. Este programa también usa la segunda herramienta de httptest. httptest.NewRecorder es un ResponseWriter que guarda lo que escribió el handler, así que puedes llamar a un handler directamente, sin nada de red:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func health(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "ok")
}
func main() {
h := http.HandlerFunc(health)
fmt.Printf("%T\n", h)
req := httptest.NewRequest("GET", "/health", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
fmt.Println(rec.Code)
fmt.Print(rec.Body.String())
}
Imprime:
http.HandlerFunc
200
ok
http.HandlerFunc(health) es una conversión de tipo, no una llamada. Nada corre hasta que se llama a ServeHTTP. httptest.NewServer prueba el viaje completo a través de una conexión. httptest.NewRecorder prueba solo el handler, lo que es más rápido. La parte sobre probar y publicar la API profundiza mucho más en los dos.
Enrutamiento con http.ServeMux
Un servidor real tiene más de un handler, así que algo tiene que elegir cuál recibe cada petición. En net/http eso es http.ServeMux, un router que a su vez es un handler. Registras patrones en él, y su propio ServeHTTP elige el handler correcto y lo llama.
Desde Go 1.22, un patrón puede nombrar un método HTTP además de una ruta, y una ruta puede tener wildcards entre llaves. Dentro del handler, r.PathValue devuelve lo que capturó un wildcard:
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /tasks/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "task %s\n", r.PathValue("id"))
})
mux.HandleFunc("DELETE /tasks/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "deleted task %s\n", r.PathValue("id"))
})
srv := httptest.NewServer(mux)
defer srv.Close()
send := func(method, path string) {
req, _ := http.NewRequest(method, srv.URL+path, nil)
res, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("error:", err)
return
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Printf("%-6s %-12s %d %q", method, path, res.StatusCode, body)
if allow := res.Header.Get("Allow"); allow != "" {
fmt.Printf(" Allow: %s", allow)
}
fmt.Println()
}
send("GET", "/tasks/42")
send("DELETE", "/tasks/7")
send("PUT", "/tasks/42")
send("GET", "/tasks")
send("GET", "/tasks/a%2Fb")
send("GET", "/users/1")
}
Imprime:
GET /tasks/42 200 "task 42\n"
DELETE /tasks/7 200 "deleted task 7\n"
PUT /tasks/42 405 "Method Not Allowed\n" Allow: DELETE, GET, HEAD
GET /tasks 404 "404 page not found\n"
GET /tasks/a%2Fb 200 "task a/b\n"
GET /users/1 404 "404 page not found\n"
Lee las líneas una por una:
GET /tasks/42yDELETE /tasks/7llegan cada uno a su propio handler, y{id}capturó el número.PUT /tasks/42encontró una ruta que existe, pero ningún patrón permitePUT. ServeMux respondió 405 Method Not Allowed por su cuenta, con un headerAllowque lista los métodos que sí funcionarían.HEADestá en la lista porque un patrónGETtambién coincide con las peticionesHEAD.GET /taskses un 404, no un 405.{id}tiene que coincidir con un segmento de la ruta, y no hay ninguno, así que ningún patrón coincide con la ruta./tasks/a%2Fbnos sorprendió.%2Fes una barra escapada, así que el wildcard ve un solo segmento. PeroPathValuelo devuelve sin escapar, comoa/b. Trata un valor de ruta como cualquier otra entrada que viene de un cliente, y valídalo antes de usarlo.- Una ruta que ningún patrón conoce recibe 404 con el cuerpo
404 page not found.
En un patrón, el método va antes de la ruta, separado por un espacio, y un patrón sin método coincide con todos los métodos. Antes de Go 1.22 nada de esto existía. La gente revisaba r.Method a mano o recurría a un router de terceros. Para la mayoría de las APIs, hoy no lo necesitas.
Explicado como si tuvieras diez años
Imagina la sala de clasificación de una oficina de correos. Hay una pared de casilleros, y cada uno tiene una dirección escrita encima: “Calle Principal 12”, “Calle Principal, cualquier casa”, “Cualquier lugar de la ciudad”. Llega una carta, y el clasificador lee su dirección y la deja en un casillero. Un cartero toma todo lo que hay en ese casillero y lo reparte.
Si una dirección cabe en más de un casillero, el clasificador elige el más exacto. Una carta para Calle Principal 12 va al casillero “Calle Principal 12”, aunque “Calle Principal, cualquier casa” también la aceptaría. Si ningún casillero sirve, la carta se devuelve con un sello de “dirección desconocida”.
La versión precisa
ServeMux guarda un conjunto de patrones, cada uno junto a un handler. Para cada petición busca todos los patrones que coinciden con el método y la ruta. Si coinciden varios, gana el más específico. Un patrón es más específico que otro si coincide con un subconjunto estricto de las peticiones con las que coincide el otro. El orden en que los registraste no importa.
Si nada coincide con la ruta, ServeMux llama a su handler interno de no encontrado, que escribe 404. Si la ruta coincide pero el método no, escribe 405 con un header Allow.
Dónde falla la analogía: una oficina de correos clasifica solo por dirección. ServeMux también lee el método, que es como clasificar por dirección y por si el sobre dice “entregar”, “recoger” o “cancelar”. Un wildcard además hace más que un casillero: copia parte de la dirección, como 42, y se la pasa al cartero. Y una oficina de correos se las arregla como puede cuando dos casilleros son igual de exactos. ServeMux se niega a arrancar, como muestra la siguiente sección.
Wildcards: {name...} y {$}
Un wildcard simple como {id} coincide con exactamente un segmento de la ruta. Dos formas especiales cubren los demás casos. {path...} al final de un patrón coincide con todos los segmentos restantes, barras incluidas. {$} coincide solo con el final de la ruta, y así es como registras la raíz sin atrapar todo lo demás:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "home page")
})
mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "file %q\n", r.PathValue("path"))
})
for _, path := range []string{"/", "/about", "/files/notes/2026/todo.txt", "/files/"} {
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, httptest.NewRequest("GET", path, nil))
fmt.Printf("%-27s %d %s", path, rec.Code, rec.Body.String())
}
}
Imprime:
/ 200 home page
/about 404 404 page not found
/files/notes/2026/todo.txt 200 file "notes/2026/todo.txt"
/files/ 200 file ""
Sin {$}, el patrón GET / terminaría en barra, y un patrón que termina en barra coincide con todas las rutas que están debajo. /about recibiría la página de inicio en lugar de un 404. El wildcard {path...} también puede no coincidir con nada, y por eso /files/ llegó al handler con un string vacío.
Gana el patrón más específico
Cuando una petición coincide con varios patrones, ServeMux no toma el primero que se registró. Toma el que coincide con la menor cantidad posible de peticiones. Este programa registra cuatro patrones que se solapan, a propósito en el orden “equivocado”:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func reply(text string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, text)
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/tasks/{id}", reply(`"/tasks/{id}"`))
mux.HandleFunc("GET /tasks/{id}", reply(`"GET /tasks/{id}"`))
mux.HandleFunc("GET /tasks/new", reply(`"GET /tasks/new"`))
mux.HandleFunc("/", reply(`"/"`))
for _, t := range []struct{ method, path string }{
{"GET", "/tasks/new"},
{"GET", "/tasks/42"},
{"PUT", "/tasks/42"},
{"GET", "/tasks/42/notes"},
} {
rec := httptest.NewRecorder()
mux.ServeHTTP(rec, httptest.NewRequest(t.method, t.path, nil))
fmt.Printf("%-4s %-16s -> %s", t.method, t.path, rec.Body.String())
}
}
Imprime:
GET /tasks/new -> "GET /tasks/new"
GET /tasks/42 -> "GET /tasks/{id}"
PUT /tasks/42 -> "/tasks/{id}"
GET /tasks/42/notes -> "/"
Un segmento literal, new, le gana a un wildcard, {id}. Un patrón con método le gana a la misma ruta sin método, así que GET va al handler específico de ese método y PUT cae en el que acepta cualquier método. / termina en barra, así que coincide con todas las rutas, y /tasks/42/notes termina ahí porque no hay nada más que sirva. Por eso tampoco hay un 405 en este programa: el patrón que atrapa todo siempre coincide.
A veces ninguno de dos patrones es más específico. GET /{kind}/42 y /tasks/{id} coinciden los dos con /tasks/42, pero cada uno también coincide con rutas con las que el otro no. Registrar los dos hace que HandleFunc entre en panic al arrancar. El mensaje nombra los dos patrones y la línea donde se registró cada uno, y luego explica:
GET /{kind}/42 and /tasks/{id} both match some paths, like "/tasks/42".
But neither is more specific than the other.
Un panic al arrancar es del tipo útil. Encuentras la ambigüedad la primera vez que corres el servidor, no cuando llega una petición con mala suerte.
Escribir una respuesta: header, código de estado, cuerpo
Una respuesta HTTP sale en un orden fijo: la línea de estado, luego los headers, luego el cuerpo. ResponseWriter te obliga a seguir ese orden, porque una vez que una parte salió, ya no se puede cambiar. Tres llamadas corresponden a las tres partes:
w.Header()devuelve el map de headers. Cámbialo primero.w.WriteHeader(code)envía la línea de estado y los headers.w.Writeenvía bytes del cuerpo. Si todavía no se llamó aWriteHeader, el primerWritellama aWriteHeader(200)por ti.
Este es un handler que lo hace en el orden correcto:
package main
import (
"fmt"
"net/http"
"net/http/httptest"
)
func create(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Location", "/tasks/43")
w.WriteHeader(http.StatusCreated)
fmt.Fprintln(w, "created task 43")
}
func main() {
rec := httptest.NewRecorder()
create(rec, httptest.NewRequest("POST", "/tasks", nil))
res := rec.Result()
fmt.Println(res.Status)
fmt.Println("Location:", res.Header.Get("Location"))
fmt.Print(rec.Body.String())
}
Imprime:
201 Created
Location: /tasks/43
created task 43
Usa rec.Result() para leer lo que vería un cliente. rec.Header() es el map vivo del handler, y todavía muestra headers que se fijaron demasiado tarde para enviarse.
Ahora el orden equivocado. Este handler escribe primero el cuerpo y después intenta fijar un header y un 404. Para ver la queja del servidor, el programa construye el servidor de prueba con httptest.NewUnstartedServer, apunta su ErrorLog a stdout sin marca de tiempo, y solo entonces lo arranca:
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/http/httptest"
"os"
)
func lateHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "task 42")
w.Header().Set("X-Task-Id", "42")
w.WriteHeader(http.StatusNotFound)
}
func main() {
srv := httptest.NewUnstartedServer(http.HandlerFunc(lateHandler))
// Send the server's error log to stdout, with no timestamp, so we can see it.
srv.Config.ErrorLog = log.New(os.Stdout, "server log: ", 0)
srv.Start()
defer srv.Close()
res, err := http.Get(srv.URL)
if err != nil {
fmt.Println("error:", err)
return
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println("status:", res.StatusCode)
fmt.Printf("X-Task-Id: %q\n", res.Header.Get("X-Task-Id"))
fmt.Print("body: ", string(body))
}
Imprime:
server log: http: superfluous response.WriteHeader call from main.lateHandler (main.go:15)
status: 200
X-Task-Id: ""
body: task 42
El cliente recibió un 200, no un 404, y ningún header X-Task-Id. El primer Fprintln fijó el código de estado en 200 y congeló los headers. El Header().Set tardío cambió un map que ya nadie lee, y lo hizo en silencio. El WriteHeader tardío no hizo nada, salvo dejar una línea “superfluous” en el log del servidor con la función y el número de línea. Si ves esa línea en un log real, busca un camino de error que escribe después de que empezó el cuerpo.
La vida de una petición
Una petición pasa por varias manos entre el cliente y tu handler, y cada una hace un solo trabajo. Mira cómo pasa una petición:
Una petición de principio a fin. El servidor le da a la conexión su propia goroutine, ServeMux elige el patrón más específico que coincide, y el handler lee el valor de la ruta y después escribe el header, el código de estado y el cuerpo, en ese orden, antes de que la respuesta vuelva.
Estos son los pasos en palabras, por si la animación no se reproduce:
- Un cliente envía
GET /tasks/42. - El servidor acepta la conexión y arranca una goroutine nueva para atenderla.
- ServeMux compara la petición con sus patrones,
GET /tasks,GET /tasks/{id}yPOST /tasks, y eligeGET /tasks/{id}. - Corre el handler de ese patrón y llama a
r.PathValue("id"), que devuelve"42". - El handler fija un header, llama a
WriteHeader(200)y escribe el cuerpo. - La respuesta vuelve al cliente, y la goroutine ya no tiene nada más que hacer.
El paso 2 importa más de lo que parece. El servidor ejecuta Accept en un bucle, y por cada conexión nueva arranca una goroutine, como en la parte sobre goroutines. Las peticiones que llegan por esa conexión se turnan en su goroutine, y con HTTP/2 cada petición recibe una goroutine propia. De cualquier forma, cien clientes significan cien goroutines corriendo tus handlers al mismo tiempo, y tú nunca escribiste go.
Este programa demuestra que dos handlers de verdad corren a la vez. /wait se bloquea hasta que se cierra un canal, y solo /release lo cierra:
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
)
func get(url string) string {
res, err := http.Get(url)
if err != nil {
return "error: " + err.Error()
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
return string(body)
}
func main() {
release := make(chan struct{})
mux := http.NewServeMux()
mux.HandleFunc("GET /wait", func(w http.ResponseWriter, r *http.Request) {
<-release // blocks until another request closes the channel
fmt.Fprint(w, "wait: released")
})
mux.HandleFunc("GET /release", func(w http.ResponseWriter, r *http.Request) {
close(release)
fmt.Fprint(w, "release: done")
})
srv := httptest.NewServer(mux)
defer srv.Close()
waitResult := make(chan string)
go func() { waitResult <- get(srv.URL + "/wait") }()
fmt.Println(get(srv.URL + "/release"))
fmt.Println(<-waitResult)
}
Imprime:
release: done
wait: released
Si el servidor atendiera una petición a la vez, /wait ocuparía al único trabajador para siempre, /release nunca correría y el programa se colgaría. Termina, así que los dos handlers estaban corriendo al mismo tiempo.
El estado compartido en un handler necesita un lock
Los handlers que corren al mismo tiempo y tocan la misma variable tienen una carrera de datos, exactamente como las goroutines de la parte sobre sync. Aquí la carrera es fácil de pasar por alto, porque nada en tu código arranca una goroutine. Este handler cuenta visitas sin lock, y llegan 50 peticiones juntas:
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"sync"
)
type visits struct {
count int
}
func (v *visits) ServeHTTP(w http.ResponseWriter, r *http.Request) {
v.count++ // no lock: every request runs in its own goroutine
fmt.Fprint(w, v.count)
}
func main() {
srv := httptest.NewServer(&visits{})
defer srv.Close()
var wg sync.WaitGroup
for range 50 {
wg.Go(func() {
res, err := http.Get(srv.URL)
if err != nil {
fmt.Println("error:", err)
return
}
io.Copy(io.Discard, res.Body)
res.Body.Close()
})
}
wg.Wait()
fmt.Println("done")
}
Si lo ejecutas con go run -race ., imprime un reporte como este (las líneas ... reemplazan direcciones de memoria, rutas de archivos y números de goroutine que cambian en cada ejecución):
WARNING: DATA RACE
...
main.(*visits).ServeHTTP()
...
net/http.(*conn).serve()
...
done
exit status 66
Mira el stack en el reporte. Debajo de tu ServeHTTP está net/http.(*conn).serve(), la goroutine que el servidor arrancó para cada conexión. El detector de carreras encontró dos de ellas escribiendo count sin nada que las coordine.
La solución es la de la parte sobre sync: un mutex en el struct, y un receptor puntero para que todas las peticiones compartan el mismo:
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"sync"
)
type visits struct {
mu sync.Mutex
count int
}
func (v *visits) ServeHTTP(w http.ResponseWriter, r *http.Request) {
v.mu.Lock()
v.count++
n := v.count
v.mu.Unlock()
fmt.Fprint(w, n)
}
func main() {
v := &visits{}
srv := httptest.NewServer(v)
defer srv.Close()
var wg sync.WaitGroup
for range 50 {
wg.Go(func() {
res, err := http.Get(srv.URL)
if err != nil {
fmt.Println("error:", err)
return
}
io.Copy(io.Discard, res.Body)
res.Body.Close()
})
}
wg.Wait()
v.mu.Lock()
fmt.Println("visits:", v.count)
v.mu.Unlock()
}
Imprime:
visits: 50
El handler copia la cuenta en n mientras tiene el lock, y lo libera antes de escribir la respuesta. Escribirle a un cliente lento puede tardar mucho, y nadie más debería esperar por eso. Todo lo que un handler lee o escribe fuera de su propia petición, como un map de tareas, una caché o un contador, necesita el mismo cuidado.
r.Context() termina cuando el cliente se va
Cada petición lleva un contexto, y el servidor lo cancela cuando el cliente se desconecta. Es el context de la parte sobre patrones de concurrencia, ya conectado para ti. Un handler que hace trabajo lento debería vigilar r.Context().Done() y parar, porque ya nadie está esperando la respuesta.
En este programa, el cliente se rinde en cuanto el handler empezó. El trabajo del handler tardaría 10 segundos, un margen amplio, así que el resultado siempre es el mismo:
package main
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"time"
)
func main() {
started := make(chan struct{})
outcome := make(chan string)
slow := func(w http.ResponseWriter, r *http.Request) {
close(started)
select {
case <-time.After(10 * time.Second):
fmt.Fprintln(w, "report ready")
outcome <- "handler: finished the work"
case <-r.Context().Done():
outcome <- "handler: stopped early: " + r.Context().Err().Error()
}
}
srv := httptest.NewServer(http.HandlerFunc(slow))
defer srv.Close()
ctx, cancel := context.WithCancel(context.Background())
go func() {
<-started
cancel() // the client gives up once the handler is running
}()
req, _ := http.NewRequestWithContext(ctx, "GET", srv.URL, nil)
_, err := http.DefaultClient.Do(req)
fmt.Println("client: canceled:", errors.Is(err, context.Canceled))
fmt.Println(<-outcome)
}
Imprime:
client: canceled: true
handler: stopped early: context canceled
Cancelar el contexto del cliente cerró la conexión. El servidor lo notó, canceló r.Context(), y el select del handler tomó el caso Done de inmediato en lugar de esperar 10 segundos. En un handler real le pasas r.Context() a la consulta a la base de datos o a la llamada saliente, y esas también se detienen. El contexto también termina cuando ServeHTTP retorna, así que no lo guardes para trabajo que deba sobrevivir a la petición.
Construye un http.Server, no llames a http.ListenAndServe
El primer servidor Go de todo tutorial es http.ListenAndServe(":8080", mux). Funciona, pero construye un http.Server con todos los campos en su valor cero, y para los timeouts, cero significa “esperar para siempre”.
Eso es un problema real en internet. Un cliente puede abrir una conexión y enviar los headers de su petición a un byte cada pocos segundos. Sin ReadHeaderTimeout, el servidor espera con paciencia, ocupando una goroutine y una conexión abierta. Con suficientes clientes así, el servidor se queda sin conexiones abiertas que pueda sostener, sin ver nunca una petición completa. El ataque es tan viejo que tiene nombre, Slowloris, y al atacante casi no le cuesta nada.
La solución es construir el servidor tú mismo y fijar los timeouts:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /tasks/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "task %s\n", r.PathValue("id"))
})
srv := &http.Server{
Addr: "localhost:8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Println("listening on", srv.Addr)
log.Fatal(srv.ListenAndServe())
}
Nuestro verificador no ejecuta este, porque escucha en un puerto fijo hasta que lo detienes. Ejecútalo tú, y desde una segunda terminal:
curl localhost:8080/tasks/42
Eso imprime task 42. Esto es lo que limita cada timeout:
ReadHeaderTimeout: cuánto tiempo tiene un cliente para enviar los headers de la petición. Es el que detiene a Slowloris, y el que nunca debes omitir.ReadTimeout: cuánto puede tardar leer la petición completa, cuerpo incluido.WriteTimeout: cuánto tiempo tiene el servidor para escribir la respuesta después de leer los headers.IdleTimeout: cuánto tiempo puede quedar sin uso una conexión keep-alive entre peticiones.
Los números de arriba son puntos de partida razonables, no reglas. Un servidor que acepta subidas grandes o transmite respuestas largas necesita otros. Además, ListenAndServe se bloquea hasta que el servidor falla, y entonces log.Fatal sale sin dejar que terminen las peticiones en curso. Elegir timeouts para producción y apagar el servidor de forma ordenada se cubren en la parte sobre probar y publicar la API.
Qué recordar
- Un
http.Handleres cualquier tipo conServeHTTP(http.ResponseWriter, *http.Request).http.HandlerFuncconvierte una función común en uno. - Desde Go 1.22, los patrones de ServeMux aceptan un método y wildcards:
"GET /tasks/{id}", que se lee conr.PathValue("id").{rest...}coincide con el resto de la ruta, y{$}coincide solo con el final. - Gana el patrón más específico, sin importar el orden de registro. Los patrones ambiguos entran en panic al registrarse. Una ruta conocida con el método equivocado recibe 405 y un header
Allow, y una ruta desconocida recibe 404. - Fija los headers, luego llama a
WriteHeader, luego escribe el cuerpo. El primerWriteenvía 200 y congela los headers, y todo lo que venga después se ignora. - Las peticiones corren en sus propias goroutines, así que el estado compartido en un handler necesita un mutex, y
-raceva a encontrar los lugares que no lo tienen. r.Context()se cancela cuando el cliente se va. Pásalo al trabajo lento.- Construye un
http.Servercon al menosReadHeaderTimeoutfijado.http.ListenAndServeespera para siempre a los clientes lentos.
Tu handler corre en muchas goroutines a la vez, las hayas arrancado tú o no.