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))
}Ejecutamos:
go run main.goY abrimos en el navegador:
http://localhost:8080http.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))
}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)
}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))
}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)
})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)
}
}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))
}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)
}
}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.