Frameworks web de Go en 2026: comparativa de net/http, chi, Gin, Echo y Fiber

Cinco pilas de HTTP de Go, un mismo equipo de pruebas, cifras reales.

Índice

Go incluye un servidor HTTP apto para producción en su biblioteca estándar, y desde Go 1.22 el enrutador integrado maneja la correspondencia de métodos y los parámetros de ruta por sí solo. La cuestión sobre qué marco de trabajo usar se ha estrechado, pero no ha desaparecido.

Cinco pilas cubren casi todas las decisiones de backend en Go para 2026: la biblioteca estándar net/http, chi, Gin, Echo y Fiber. Cada una ocupa una posición distinta en el mismo dilema: lo que la biblioteca te ofrece frente a lo que le debes en dependencias, convenciones y compatibilidad con el ecosistema. Este artículo las compara en potencia de enrutamiento, ergonomía de los manejadores, middleware y rendimiento medido, y termina con una tabla de decisión en lugar de un veredicto único.

Si estás partiendo de cero y deseas el proceso completo de construcción alrededor de tu elección —validación, autenticación, pruebas, despliegue—, la versión paso a paso está en Construcción de APIs REST en Go. Aquí el enfoque se centra en las propias pilas.

five 3D glass blocks connected by glowing lines, one per Go HTTP stack

El reinicio de ServeMux post-1.22

La razón por la que el debate sobre marcos de trabajo cambió de forma es una única adición a la biblioteca estándar. Go 1.22 dotó a http.ServeMux de patrones conscientes de los métodos y parámetros de ruta:

mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")
    // ...
})
mux.HandleFunc("POST /users", createUser)

Antes de esto, cualquier API que necesitara GET /users/{id} requería un enrutador de terceros, y así es como Gin, chi y Echo se convirtieron en los valores predeterminados. En 2026, la biblioteca estándar cubre rutas estáticas, correspondencia de métodos, parámetros de segmento único y comodines, y middleware mediante envolturas de funciones simples. Lo que aún no te ofrece: grupos de rutas, cadenas de middleware componibles, unión de estructuras o tipos de parámetros por ruta.

Los contendientes

net/http (biblioteca estándar) chi v5.3.2 Gin v1.12.0 Echo v4.16.0 Fiber v2.52.15
Motor HTTP net/http net/http net/http net/http fasthttp
Firma del manejador func(w, r) func(w, r) func(c *gin.Context) func(c echo.Context) error func(c *fiber.Ctx) error
Compatible con http.Handler sí sí sí sí no
Ecosistema de middleware en crecimiento, con forma de biblioteca estándar con forma de biblioteca estándar el más grande, específico de Gin grande, específico de Echo en crecimiento, específico de fasthttp
Unión/validación manual manual etiquetas integradas integrada mediante auxiliares
HTTP/2 sí sí sí sí no
Dependencias 0 1 ~6 ~8 ~4

Los cinco utilizan enrutadores de estilo trie o radix con parámetros de ruta y rutas agrupadas. La división estructural es la última fila de la historia de la firma del manejador: chi y la biblioteca estándar hablan en http.Handler simple; Gin, Echo y Fiber envuelven las solicitudes en su propio tipo de contexto. Fiber es, una vez más, la excepción, porque se basa en fasthttp y no en net/http en absoluto.

Lo que puse a prueba

En lugar de citar benchmarks sintéticos de enrutadores, ejecuté el mismo servicio en las cinco pilas. El montaje:

  • Go 1.27.1, Gin v1.12.0, chi v5.3.2, Echo v4.16.0, Fiber v2.52.15
  • Intel Core Ultra 7 265, Linux, 20 hilos, localhost, sin TLS
  • Tres rutas por pila: GET /health, GET /users/{id}, GET /api/v1/users/{uid}/orders/{oid}
  • Carga: 8 workers de keep-alive durante 3 segundos contra la ruta de dos parámetros, después de una pasada de calentamiento
  • Cada manejador serializa un pequeño documento JSON por solicitud con encoding/json — así que los números incluyen la serialización, no solo el enrutamiento

La versión de la biblioteca estándar de la ruta crítica:

mux.HandleFunc("GET /api/v1/users/{uid}/orders/{oid}", func(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.Write(orderBody(r.PathValue("uid"), r.PathValue("oid")))
})

y el equivalente en Fiber:

app.Get("/api/v1/users/:uid/orders/:oid", func(c *fiber.Ctx) error {
    c.Set("Content-Type", "application/json")
    return c.Send(orderBody(c.Params("uid"), c.Params("oid")))
})

Dos notas de honestidad antes de los números. Primero, esta es una micro-carga de trabajo en localhost: sin base de datos, sin negociación TLS, sin cadena de middleware. Los servicios reales se atascan en la entrada/salida — la cuota del marco de trabajo en una solicitud que consulta PostgreSQL mediante GORM, Ent, Bun o sqlc es un error de redondeo. Segundo, cada pila tuvo su propio proceso de servidor y una forma de manejador idéntica, por lo que la comparación es entre pilas, no entre configuraciones optimizadas manualmente.

Resultados

Punto de rendimiento en la ruta dinámica de dos parámetros:

Pila Solicitudes/segundo Latencia promedio
Fiber (fasthttp) 205.958 0,034 ms
Echo 169.826 0,042 ms
Gin 169.715 0,042 ms
net/http ServeMux 169.708 0,042 ms
chi 163.017 0,044 ms

Las cuatro pilas basadas en net/http caen dentro de un 4% entre sí. El motor fasthttp de Fiber está unos 21% por delante en esta carga de trabajo en localhost — real, pero del tipo de brecha que un solo viaje a la base de datos (0,5–5 ms) hace irrelevante en producción.

Para ver dónde va realmente el coste por solicitud, también hice un benchmark de la invocación del manejador sin red, llamando a cada enrutador directamente a través de http.Handler.ServeHTTP (Fiber a través de su manejador fasthttp):

Pila Tiempo/operación Bytes/operación Asignaciones/operación
Gin 289 ns 192 B 3
Echo 333 ns 192 B 3
net/http ServeMux 430 ns 240 B 5
Fiber 541 ns* 192 B 3
chi 592 ns 897 B 7

* La cifra de Fiber incluye el sobrecoste del armazón de prueba — la prueba tiene que copiar la solicitud al contexto reutilizable de fasthttp en cada iteración, lo cual el servidor real evita leyendo del cable. Trátalo como un límite superior; el punto de rendimiento a nivel de servidor anterior es la comparación justa para Fiber.

El perfil de asignaciones es la historia más duradera. Gin y Echo pondean sus contextos de solicitud, por lo que una solicitud a través de ellos cuesta 3 asignaciones. Las 5 asignaciones de la biblioteca estándar van a la misma plomería de alcance de la solicitud. chi almacena los parámetros de ruta en el context.Context de la solicitud, lo que aparece como 897 B y 7 asignaciones por solicitud — el precio de mantenerse un http.Handler simple con información rica de la ruta.

Fiber y el intercambio con fasthttp

Fiber es la pila más rápida en el benchmark del servidor, y la velocidad viene con una factura de compatibilidad. fasthttp es una implementación HTTP desde cero con sus propios tipos de solicitud/respuesta, por lo que:

  • El middleware de net/http no se adjunta. Cualquier biblioteca que espere un http.Handler o un *http.Request necesita una conversión o un port nativo de Fiber.
  • Sin HTTP/2. fasthttp no lo implementa; net/http de Go obtiene h2 de la biblioteca estándar gratis sobre TLS.
  • WebSocket, streaming y trucos estilo http.Flusher todos toman el camino fasthttp, que difiere del comportamiento de la biblioteca estándar que asume la mayoría de la documentación de Go.

Lo que Fiber compra a cambio: las cifras más rápidas de la tabla anterior, una API similar a Express y menos asignaciones de lo que sugieren los benchmarks del marco de trabajo una vez se incluye la ventaja de análisis de cable. Encaja cuando las respuestas son pequeñas, el enrutamiento domina, HTTP/2 se termina aguas arriba y el middleware que necesitas existe en el ecosistema de Fiber. Esas condiciones se cumplen para algunos servicios de borde y se cumplen para casi nada más.

chi: el camino medio aburrido

chi es un enrutador y una tubería de middleware, nada más. Los manejadores son func(w, r), el middleware es func(http.Handler) http.Handler, y todo lo escrito para la biblioteca estándar se adjunta sin adaptadores:

r := chi.NewRouter()
r.Use(RequestID, RealIP, Logger, Recoverer)
r.Route("/api/v1", func(r chi.Router) {
    r.Get("/users/{uid}/orders/{oid}", getOrder)
})

Ese es todo el argumento: grupos de rutas y una pila de middleware sobre la interfaz estándar. La tabla de asignaciones lo precio con honestidad — un contexto de ruta más rico por solicitud que la biblioteca estándar sola, a cambio de middleware componible y rutas anidadas. Para los servicios que quieren estructura sin una identidad de marco de trabajo, chi junto con net/http es la configuración en la que la mayoría de los equipos de Go convergen.

Gin vs Echo, en código

Gin y Echo son casi gemelos en rendimiento — dentro del ruido en ambos benchmarks — por lo que la elección entre ellos es cuestión de gusto de API y filosofía de manejo de errores.

Echo centraliza los errores en un HTTPErrorHandler. Los manejadores devuelven error, y una única función mapea errores a respuestas:

func getOrder(c echo.Context) error {
    o, err := store.Order(c.Param("uid"), c.Param("oid"))
    if err != nil {
        return echo.NewHTTPError(http.StatusNotFound, "order not found")
    }
    return c.JSON(http.StatusOK, o)
}

Los manejadores de Gin no devuelven nada; los errores se acumulan en el contexto y los renderizas donde tú elijas:

func getOrder(c *gin.Context) {
    o, err := store.Order(c.Param("uid"), c.Param("oid"))
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{"error": "order not found"})
        return
    }
    c.JSON(http.StatusOK, o)
}

El modelo de Echo se mapea limpiamente a la arquitectura de manejo de errores de Go con errores envueltos y manejo de límite centralizado — un único lugar traduce los resultados de errors.Is en códigos de estado. El modelo de Gin coincide con el patrón más común de decidir por manejador. Más allá de los errores, las ventajas decisivas de Gin son el tamaño del ecosistema y la unión de estructuras integrada con etiquetas de validación; las de Echo son una API de contexto consistente y un middleware integrado más robusto. Ambas integran la generación de Swagger a través de sus propios adaptadores — la configuración por marco de trabajo está cubierta en Añadiendo Swagger a tu API de Go.

Un eje más: propagación de contexto. Gin y Echo envuelven context.Context dentro de sus propios tipos de contexto, por lo que pasar la cancelación a los manejadores requiere un paso de extracción. Los patrones para mantener la cancelación, los tiempos de espera y los valores bien organizados a través de cualquiera de estos envoltorios están en Go context.Context bien hecho.

Los tiempos de espera que nadie configura

Cada pila en esta comparación deja los tiempos de espera del servidor en valores predeterminados de cero si no los configuras — y cero significa sin límite. Un cliente que abre una conexión y no envía nada retiene una goroutine y un descriptor de archivo indefinidamente:

srv := &http.Server{
    Addr:              ":8080",
    Handler:           mux,
    ReadHeaderTimeout: 5 * time.Second,
    ReadTimeout:       10 * time.Second,
    WriteTimeout:      30 * time.Second,
    IdleTimeout:       120 * time.Second,
}
srv.ListenAndServe()

ReadHeaderTimeout es el primero que debes configurar — limita el agotamiento de conexiones estilo slowloris sin romper las cargas largas de la misma manera que puede hacerlo un ReadTimeout global. WriteTimeout debe cubrir la respuesta legítima más lenta, lo que lo convierte en una decisión de política ligada a tus presupuestos de tiempo de espera y no en una constante de copiar y pegar. La misma disciplina aplica por marco de trabajo: Gin, Echo y Fiber aceptan todos la configuración de tiempos de espera a nivel de servidor, y ninguno la habilita por defecto.

Eligiendo

flowchart TD A[Nuevo servicio HTTP en Go] --> B{¿Herramienta interna, plugin,
o requisito de cero dependencias?} B -- sí --> C[net/http ServeMux] B -- no --> D{¿Quieres unión integrada,
validación, gran catálogo de middleware?} D -- no --> E[chi + net/http] D -- sí --> F{¿Convención del equipo?} F -- manejadores que devuelven error --> G[Echo] F -- mayor ecosistema, idiomáticos de Gin --> H[Gin] A --> I{¿Respuestas pequeñas, rendimiento extremo,
sin HTTP/2, terminación TLS aguas arriba?} I -- todo cierto --> J[Fiber] I -- no --> E
Situación Elección
Servicio interno, punto de final de CLI, plugin, biblioteca net/http solo
La mayoría de las APIs — estructura sin bloqueo de marco de trabajo chi sobre net/http
API pública, equipo más grande, validación y unión lista para usar Gin
API pública, traducción centralizada de errores, API de contexto consistente Echo
Cuello de botella probado de enrutamiento/serialización, JSON pequeño, sin HTTP/2 Fiber

Si el valor predeterminado honesto para un nuevo servicio en 2026 debe nombrarse: comienza con net/http, añade chi cuando el middleware o los grupos de rutas comiencen a doler, y muévete a Gin o Echo solo cuando la unión, la validación o la convención del equipo justifique una identidad de marco de trabajo. Fiber se mantiene como una herramienta especializada — vale la pena hacer un benchmark contra ella cuando el rendimiento se ha demostrado ser el problema, evaluada contra la factura de compatibilidad que cobra.

Migrando entre pilas

El coste de migración sigue la división de la firma del manejador. La migración de biblioteca estándar ↔ chi es mecánica: los manejadores mantienen la misma firma, las rutas se mueven desde patrones de mux.HandleFunc a r.Get, y el middleware se adjunta a la pila de chi. Gin ↔ Echo es una traducción de tipos de contexto — c.Param, c.JSON y la lógica del middleware se portan casi línea por línea. Cualquier cosa que se mueva a o desde Fiber es una reescritura de la capa HTTP, porque el código basado en *http.Request, el middleware de net/http y los supuestos de h2 no sobreviven al límite de fasthttp; la ruta menos dolorosa es mantener la lógica de negocio en paquetes libres de marco de trabajo y regenerar la capa de transporte, que es también la estructura que hace la siguiente migración barata.

Enlaces útiles

Suscribirse

Recibe nuevas publicaciones sobre sistemas, infraestructura e ingeniería de IA.