Un servidor en unas pocas líneas
Un handler es una función que recibe la petición y escribe la respuesta. ServeMux enruta las peticiones a los handlers.
El editor no puede aceptar conexiones desde tu navegador, así que los ejemplos de esta página arrancan el servidor con httptest.NewServer y lo llaman desde el mismo programa. En un programa real, la última parte se sustituye por una línea que se bloquea y sirve para siempre:
log.Fatal(http.ListenAndServe(":8080", mux))
Entonces curl localhost:8080/hello/gopher imprime Hello, gopher!. ListenAndServe solo retorna ante un error (el puerto está ocupado, por ejemplo), y por eso va envuelto en log.Fatal.
Cada petición se ejecuta en su propia goroutine. Todo lo que compartan tus handlers, como un map o un contador, necesita un mutex.
Handlers
Cualquier cosa con un método ServeHTTP(http.ResponseWriter, *http.Request) es un http.Handler. http.HandlerFunc adapta una función normal a esa interfaz, y mux.HandleFunc hace la conversión por ti. Un handler basado en struct viene bien cuando los handlers necesitan dependencias:
type API struct {
db *sql.DB
}
func (a *API) listItems(w http.ResponseWriter, r *http.Request) { /* uses a.db */ }
mux.HandleFunc("GET /items", api.listItems)
El *http.Request te da:
| Campo o método | Contiene |
|---|---|
r.Method | GET, POST, ... |
r.URL.Path | la ruta, /items/42 |
r.PathValue("id") | un comodín del patrón de la ruta (Go 1.22) |
r.URL.Query().Get("q") | un parámetro de la query string |
r.Header.Get("Authorization") | una cabecera de la petición |
r.Body | el body de la petición, un io.ReadCloser (el servidor lo cierra) |
r.FormValue("name") | un campo de formulario o un parámetro de consulta |
r.Context() | un context que se cancela cuando el cliente se desconecta |
Patrones de ruta (Go 1.22)
Desde Go 1.22, los patrones de ServeMux tienen la forma [METHOD ][HOST]/[PATH], y las rutas pueden contener comodines.
| Patrón | Coincide con |
|---|---|
"/items/" | /items/ y todo lo que cuelga de ella (barra final = prefijo) |
"/items" | solo /items |
"GET /items/{id}" | GET (y HEAD) sobre /items/42; r.PathValue("id") == "42" |
"POST /items" | solo POST sobre /items |
"/files/{path...}" | /files/a/b/c; path vale "a/b/c" |
"/{$}" | solo /, no cualquier ruta |
"/" | cualquier ruta con la que no coincida ningún otro patrón |
Cuando coinciden dos patrones, gana el más específico, así que /items/new gana a /items/{id}. Si ninguno es más específico, por ejemplo /items/{id} y /{kind}/new (los dos coinciden con /items/new), registrar el segundo provoca un panic con un mensaje que nombra ambos patrones. Si una ruta coincide pero el método no, el mux responde 405 Method Not Allowed con una cabecera Allow, sin código tuyo.
El patrón "/" es el comodín general. Eso sorprende a quien registra una página de inicio con "/" y ve que responde a cualquier URL desconocida con un 200. Usa "GET /{$}" para la página de inicio.
Estos patrones necesitan go 1.22 o posterior en go.mod. Con una línea de versión más antigua, el mux vuelve al comportamiento antiguo: lee "GET /items" como un nombre de host seguido de una ruta, así que la ruta nunca coincide y todas las peticiones a /items reciben un 404.
Una pequeña API JSON
La última petición usa DELETE, que ninguna ruta acepta, y el mux responde 405 con Allow: GET, HEAD por su cuenta: esos son los métodos registrados para esa ruta (una ruta GET también acepta HEAD).
Códigos de estado y el orden de las escrituras
Una respuesta tiene tres partes, y deben escribirse en orden: cabeceras, estado, body.
w.Header().Set(...)cambia las cabeceras. Solo tiene efecto hasta que se envía el estado.w.WriteHeader(code)envía la línea de estado y las cabeceras.w.Write(...)(ofmt.Fprint(w, ...), o un encoder) envía el body. Si no se ha llamado aWriteHeader, el primerWriteenvía antes200 OK.
Consecuencias:
- Poner una cabecera después de que haya empezado el body no hace nada.
- Llamar dos veces a
WriteHeaderregistrahttp: superfluous response.WriteHeader cally mantiene el primer estado. - Después de una respuesta de error, haz
return.http.Errorno detiene tu handler, y el código que va detrás sigue escribiendo en la misma respuesta.
Usa las constantes con nombre (http.StatusOK, http.StatusCreated, http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError) en lugar de números a secas. http.StatusText(404) devuelve "Not Found".
Middleware
Un middleware es una función que recibe un handler y devuelve un handler, y hace algo antes o después de llamar al siguiente:
La línea log: siempre aparece antes que client got: para la misma petición. La goroutine del handler la imprime antes de retornar, y el servidor no termina la respuesta hasta que el handler retorna.
El statusRecorder incrusta el ResponseWriter real y sobrescribe solo WriteHeader, que es la forma estándar de observar el código de estado desde un middleware.
Ajustes de producción
http.ListenAndServe usa un servidor sin timeouts, así que un cliente lento puede mantener una conexión abierta indefinidamente. Configura un http.Server explícitamente para todo lo que esté expuesto a internet, y apágalo de forma ordenada:
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 120 * time.Second,
}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done() // wait for Ctrl+C or a SIGTERM from the orchestrator
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { // finish in-flight requests
log.Println("shutdown:", err)
}
Shutdown deja de aceptar conexiones nuevas y espera a que terminen las peticiones activas, hasta el deadline del context. ListenAndServe devuelve http.ErrServerClosed en cuanto empieza Shutdown, y por eso ese error no se trata como un fallo. Para handlers de larga duración, pasa r.Context() hacia abajo para que se detengan cuando el cliente se va.
Servir archivos estáticos es una línea: mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public")))).
Errores comunes
- No retornar después de
http.Error. El handler sigue ejecutándose y escribe más en la respuesta. - Poner cabeceras después de escribir el body. Se descartan en silencio.
- Registrar la página de inicio en
"/". Se convierte en el comodín de todas las rutas desconocidas. Usa"/{$}". - Compartir estado entre handlers sin un lock. Las peticiones se ejecutan de forma concurrente.
- Usar el servidor por defecto en producción. No tiene timeouts; ponlos en un
http.Server. - Patrones de ruta ignorados. Si
"GET /x/{id}"nunca coincide, comprueba quego.moddigago 1.22o posterior.
Preguntas frecuentes
¿Cómo creo un servidor web sencillo en Go?
Registra un handler y empieza a escuchar: http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }) y luego log.Fatal(http.ListenAndServe(":8080", nil)). El servidor de la librería estándar está listo para producción; gestiona HTTP/1.1, HTTP/2 sobre TLS, keep-alive y una goroutine por conexión.
¿Cómo obtengo un parámetro de la ruta en net/http de Go?
Desde Go 1.22, los patrones de ServeMux pueden contener comodines: registra mux.HandleFunc("GET /items/{id}", h) y lee el valor dentro del handler con r.PathValue("id"). Un {path...} al final coincide con el resto de la ruta. Antes de 1.22 tenías que partir r.URL.Path tú mismo o usar un router como chi.
¿Necesito un framework como Gin para construir una API REST en Go?
No. Desde Go 1.22, net/http enruta por método y por parámetros de ruta, que era el motivo principal para usar routers. Con encoding/json para los bodies y pequeñas funciones de middleware para logs y autenticación, la librería estándar basta para la mayoría de APIs. Los frameworks añaden comodidades como el binding de peticiones y la validación.
¿Cómo pongo el código de estado en un handler HTTP de Go?
Llama a w.WriteHeader(http.StatusCreated) antes de escribir el body. Si escribes primero el body, Go envía 200 OK automáticamente y un WriteHeader posterior se ignora con el mensaje de log superfluous response.WriteHeader call. Pon también las cabeceras con w.Header().Set antes de WriteHeader; para errores, http.Error(w, msg, code) lo hace todo.