← назад к разделу

Стандартный net/http удобен и совместим со всей экосистемой, но у него есть устроенная в него цена: на каждый входящий запрос он создаёт новый объект http.Request с новыми картами заголовков и свежими срезами для тела. На обычных нагрузках это не заметно — сборщик мусора Go справляется. Но когда сервис должен отвечать сотнями тысяч мелких ответов в секунду, аллокации становятся главным расходом: это работа для GC и давление на кэш процессора.

Для такого случая есть другой путь — библиотека fasthttp, а поверх неё фреймворк Fiber. Разберём, как они выигрывают скорость и чем за это приходится платить.

Обязательно

Как fasthttp выигрывает: переиспользование вместо аллокаций

Идея fasthttp проста и радикальна: запрос не создаётся заново, а разбирается прямо в переиспользуемом буфере. Байты из сокета не копируются в свежие структуры — парсер кладёт заголовки и тело в заранее выделенную память, которая после ответа отдаётся следующему запросу. Результат — почти ноль аллокаций на горячем пути, и именно она даёт Fiber его бенчмарки.

// так это выглядит в fasthttp: обработчик получает объекты, которые
// принадлежат серверу и живут только пока выполняется этот вызов
func handler(ctx *fasthttp.RequestCtx) {
    name := ctx.QueryArgs().Peek("name")
    ctx.SetBodyString("hello, " + string(name))
}

Обратите внимание на следствие, из которого вырастают все ограничения: память, куда разобран запрос, переиспользуется. Следующий запрос запишет свои заголовки туда же. Поэтому указатель на ctx или на тело запроса нельзя выпускать за пределы обработчика — данные будут перезаписаны, молча и непредсказуемо.

net/http: новый объект на каждый запрос fasthttp: один буфер на все запросы Request 1 Request 2 Request 3 куча растёт GC собирает позже аллокации на каждый запрос — работа для сборщика буфер запроса парсим запрос 1 → отвечаем → буфер свободен аллокаций на горячем пути почти нет запрос 1 перезаписан запросом 2 указатель на ctx после return — мусор или чужие данные цена: жизнь запроса — только время обработчика

Три такта одной идеи: net/http платит аллокациями за удобство, fasthttp платит совместимостью за скорость.

Fiber: знакомый API поверх fasthttp

Fiber — это роутер и набор middleware поверх fasthttp, стилистически повторяющий Express из мира Node.js. Если вы видели app.get("/", handler) в JavaScript — Fiber выглядит так же, и именно поэтому он популярен у команд, пришедших из фронтенда:

app := fiber.New()

app.Get("/hello", func(c *fiber.Ctx) error {
    return c.JSON(fiber.Map{"message": "hello"})
})

app.Listen(":3000")

Типичный стек такого сервиса: Fiber v2, pgx для PostgreSQL, goose или migrate для миграций, Kubernetes наружу. По сути это тот же слоистый сервис controller/usecase/repository, что и на chi — меняется только транспортный слой.

Цена: три ограничения, о которых спрашивают на собеседовании

Несовместимость с net/http. fiber.Ctx — это не http.ResponseWriter и *http.Request. Весь паравоз экосистемы, построенный на http.Handler — стандартные middleware, httptest, OTel-инструментация otelhttp, pprof-обработчики, — либо недоступен, либо требует переходников, которые съедают часть выигрыша. Это главное архитектурное следствие, и оно не лечится «просто подключить библиотеку».

Время жизни запроса. Вспомним анимацию: буфер переиспользуется. Поэтому классическая ошибка — сохранить контекст, отдать тело запроса в горутину или положить его в кэш:

// так не надо: ctx и его тело будут перезаписаны следующим запросом
app.Post("/orders", func(c *fiber.Ctx) error {
    go processAsync(c) // c перестанет быть валидным сразу после return
    return c.SendStatus(fiber.StatusAccepted)
})
// так надо: скопировать нужные данные в свою память до возврата
app.Post("/orders", func(c *fiber.Ctx) error {
    body := bytes.Clone(c.Body()) // свежая память, принадлежит нам
    go processAsync(body)
    return c.SendStatus(fiber.StatusAccepted)
})

Порог входа для команды. Отладка, профилирование и привычные инструменты рассчитаны на стандартную библиотеку. Уходя на fasthttp, команда должна понимать его модель памяти, иначе гонки и порча данных будут находиться только в проде.

Проверим, как контрастирует net/http: там запрос можно спокойно хранить, объект принадлежит вам. Этот пример выполняется сам по себе:

живой пример

package main

import (
    "fmt"
    "net/http"
    "net/http/httptest"
)

func main() {
    req := httptest.NewRequest("GET", "/hello", nil)
    rec := httptest.NewRecorder()
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "hello")
    })
    handler.ServeHTTP(rec, req)

    saved := req // объект остаётся валидным после обработки
    fmt.Println(saved.Method, rec.Body.String())
}
Запустить

Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →

Дополнительно: при первом чтении можно пропустить

Глубже: как читать бенчмаркирасширенное

В синтетических тестах «hello world» Fiber опережает net/http в разы по числу запросов в секунду. К таким цифрам нужно относиться как к спецификации двигателя на оборотах без нагрузки: в реальном сервисе время уходит в базу, сериализацию JSON и сеть, а транспорт — это один-два процента бюджета. Fiber оправдан там, где сам транспорт реально узкое место: пуш-шлюзы, прокси, игровые бэкенды, API, отдающие одинаковые мелкие ответы миллионам клиентов. Для типового бизнес-сервиса с SQL и сетевыми вызовами разница будет в пределах шума — а цена совместимости останется.

Коротко

  • fasthttp выигрывает за счёт переиспользования буферов — почти ноль аллокаций на запрос.
  • fiber.Ctx нельзя хранить: память запроса перезаписывается следующим — копируйте данные до return.
  • Fiber несовместим с http.Handler: экосистема net/http, httptest и otelhttp недоступны напрямую.
  • Берут Fiber ради API как в Express и максимального RPS на мелких ответах; типовой бизнес-сервис его преимущества не почувствует.
  • Правило выбора: начинайте с chi поверх net/http, уходите на fasthttp только при измеренной необходимости.

Что почитать дальше