Описание пакета для управления маршрутизацией github.com/julienschmidt/httprouter для GO

HttpRouter — это легкий высокопроизводительный маршрутизатор HTTP-запросов (также называемый мультиплексором или просто мультиплексором) для Go.

HttpRouter

HttpRouter — это легковесный высокопроизводительный маршрутизатор HTTP-запросов (также называемый мультиплексором или просто “mux”) для Go.

В отличие от стандартного мультиплексора пакета net/http в Go, этот роутер поддерживает:

  • Переменные в шаблонах маршрутов
  • Соответствие HTTP-методам запроса
  • Лучшую масштабируемость

Роутер оптимизирован для высокой производительности и малого потребления памяти. Эффективно работает даже с очень длинными путями и большим количеством маршрутов благодаря использованию сжимающей динамической trie-структуры (радиксное дерево).


Ключевые особенности

1. Только точные совпадения
В отличие от других роутеров (например, http.ServeMux), где URL может соответствовать нескольким шаблонам (с приоритетами типа “самое длинное совпадение” или “первый зарегистрированный — первый обработанный”), здесь запрос может соответствовать только одному маршруту или ни одному. Это исключает неожиданные совпадения, что полезно для SEO и UX.

2. Автоматическая обработка слэшей
Роутер автоматически перенаправляет при отсутствии или избытке trailing slash (косой черты в конце пути), если для нового пути есть обработчик. Можно отключить.

3. Коррекция пути
Дополнительные возможности:

  • Исправление регистра символов (например, для CAPTAIN CAPS LOCK)
  • Удаление избыточных элементов (../, //)
  • Case-insensitive поиск с редиректом

4. Параметры в путях
Динамические сегменты пути (например, /user/:id) извлекаются без ручного парсинга URL. Реализовано с минимальными накладными расходами.

5. Zero Garbage
Процесс сопоставления и диспетчеризации не создает мусора (zero bytes allocation). Единственные выделения памяти:

  • Создание slice для параметров пути
  • Создание контекста и объекта запроса (только в стандартном Handler API)
  • В 3-аргументном API при отсутствии параметров — аллокаций нет вообще.

6. Максимальная производительность
Реализация оптимизирована (см. бенчмарки). Используется радиксное дерево для эффективного сопоставления длинных путей.

7. Обработка паник
Можно установить PanicHandler для перехвата паник во время обработки запроса. Роутер восстановит работу и отправит клиенту ошибку.

8. Идеально для API

  • Поощряет построение RESTful API с иерархической структурой
  • Нативная поддержка OPTIONS-запросов
  • Автоматические ответы 405 Method Not Allowed
  • Возможность кастомизации NotFound и MethodNotAllowed обработчиков
  • Поддержка статических файлов

Технические детали

Используется радиксное дерево (trie) с компрессией для:

  • Эффективного хранения длинных путей
  • Быстрого поиска даже при тысячах маршрутов
  • Минимизации использования памяти

Возможности

Именованные параметры

Как видно из примеров, :name представляет собой именованный параметр. Его значения доступны через httprouter.Params — это срез (slice) параметров. Получить значение можно двумя способами:

  1. По индексу в срезе
  2. Через метод ByName(name): например, параметр :name извлекается вызовом ByName("name")

Важно:
При использовании стандартного http.Handler (через router.Handler или http.HandlerFunc) вместо 3-аргументного API HttpRouter, именованные параметры хранятся в контексте запроса (request.Context). Подробнее см. раздел «Почему это не работает с http.Handler?».

Особенности именованных параметров:

  • Соответствуют только одному сегменту пути
    Пример шаблона: /user/:user
Путь Совпадение
/user/gordon
/user/you
/user/gordon/profile
/user/

Ограничение:
Так как роутер использует только явные совпадения, нельзя зарегистрировать одновременно статический маршрут и параметр для одного сегмента. Например, эти шаблоны не могут сосуществовать для одного HTTP-метода:

  • /user/new (статический)
  • /user/:user (параметр)

При этом маршрутизация для разных методов запроса (GET, POST и т.д.) обрабатывается независимо.


Параметры Catch-All (перехват всего)

Имеют формат *name и, как следует из названия, соответствуют любому пути. Всегда должны находиться в конце шаблона.

Пример шаблона: /src/*filepath

Путь Совпадение
/src/
/src/somefile.go
/src/subdir/somefile.go

Особенности:

  • Перехватывают все оставшиеся сегменты пути, включая слэши
  • Полезны для реализации «обработчиков-прокси» (например, статических файлов в подкаталогах)

Как это работает?

Роутер использует древовидную структуру, активно использующую общие префиксы — по сути, это компактное префиксное дерево (радиксное дерево). Узлы с общим префиксом имеют общего родителя. Вот пример структуры дерева для GET-запросов:

Приоритет   Путь             Обработчик
9          \                *<1>
3          ├s               nil
2          |├earch\         *<2>
1          |└upport\        *<3>
2          ├blog\           *<4>
1          |    └:post      nil
1          |\     *<5>
2          ├about-us\       *<6>
1          |        └team\  *<7>
1          └contact\        *<8>

Здесь каждый *<num> представляет адрес обработчика (указатель на функцию). При прохождении пути от корня до листа формируется полный маршрут, например \blog\:post\, где :post — это параметр-заполнитель. В отличие от хэш-таблиц, дерево позволяет работать с динамическими параметрами, поскольку сопоставление происходит по шаблонам, а не через сравнение хэшей. Бенчмарки подтверждают эффективность этого подхода.

Оптимизации структуры:

  1. Иерархичность URL:
    Ограниченный набор символов в путях URL создает множество общих префиксов, что позволяет декомпозировать задачу маршрутизации на меньшие подзадачи.
  2. Раздельные деревья для методов:
    Для каждого HTTP-метода (GET, POST и т.д.) строится отдельное дерево. Это:
    • Экономит память (не требует хранения map[метод]->обработчик в каждом узле).
    • Сокращает пространство поиска до актуального метода.
  3. Приоритизация узлов:
    Дочерние узлы сортируются по приоритету — количеству обработчиков в поддеревьях. Это дает:
    • Быстрый доступ к популярным маршрутам.
    • Приоритетную обработку длинных путей (с максимальной «стоимостью»).

Визуализация приоритетов:

├------------
├---------
├-----
├----
├--
├--
└-

Почему это не работает с http.Handler?

Работает! Роутер сам реализует интерфейс http.Handler и предоставляет адаптеры для интеграции стандартных http.Handler и http.HandlerFunc в качестве httprouter.Handle.

Доступ к параметрам:

Для http.Handler именованные параметры доступны через контекст запроса:

func Hello(w http.ResponseWriter, r *http.Request) {
    params := httprouter.ParamsFromContext(r.Context()) 
    fmt.Fprintf(w, "hello, %s!\n", params.ByName("name"))
}

Альтернативный вариант:

params := r.Context().Value(httprouter.ParamsKey)

Автоматические OPTIONS-ответы и CORS

Для кастомизации автоматических ответов на OPTIONS-запросы (например, для поддержки CORS preflight или добавления заголовков) используйте обработчик Router.GlobalOPTIONS:

router.GlobalOPTIONS = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    // Если это CORS preflight запрос
    if r.Header.Get("Access-Control-Request-Method") != "" {
        header := w.Header()
        // Разрешаем методы из заголовка Allow
        header.Set("Access-Control-Allow-Methods", r.Header.Get("Allow")) 
        // Открываем доступ для всех доменов
        header.Set("Access-Control-Allow-Origin", "*")  
    }
    // Возвращаем статус 204 No Content
    w.WriteHeader(http.StatusNoContent)  
})

Где найти Middleware X?

HttpRouter — это исключительно высокопроизводительный роутер с минималистичным функционалом. Поскольку он реализует интерфейс http.Handler, вы можете:

  1. Цеплять любые совместимые middleware перед роутером (например, из библиотеки Gorilla).
  2. Создавать собственные middleware — это достаточно просто.
  3. Использовать фреймворки на основе HttpRouter.

Мультидомены и поддомены

Пример реализации маршрутизации для разных доменов/поддоменов:

// HostSwitch - карта для хранения обработчиков по доменам
type HostSwitch map[string]http.Handler

// Реализуем интерфейс http.Handler
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // Ищем обработчик для текущего хоста
    if handler := hs[r.Host]; handler != nil {
        handler.ServeHTTP(w, r)
    } else {
        http.Error(w, "Forbidden", http.StatusForbidden) // Или редирект
    }
}

func main() {
    // Инициализация роутера
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)

    // Настройка HostSwitch
    hs := make(HostSwitch)
    hs["example.com:12345"] = router  // Для основного домена
    hs["api.example.com:12345"] = anotherRouter  // Для поддомена

    // Запуск сервера
    log.Fatal(http.ListenAndServe(":12345", hs))
}

Ключевые моменты:

  • Каждый домен/поддомен может иметь собственный роутер
  • Порт указывается вместе с доменом (example.com:12345)
  • Для необрабатываемых доменов возвращается 403 Forbidden (можно заменить на редирект)

Базовая аутентификация (Basic Auth)

Пример реализации HTTP Basic Authentication (RFC 2617) для обработчиков:

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/julienschmidt/httprouter"
)

// BasicAuth - middleware для проверки учетных данных
func BasicAuth(h httprouter.Handle, requiredUser, requiredPassword string) httprouter.Handle {
	return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
		// Получаем учетные данные из заголовка
		user, password, hasAuth := r.BasicAuth()

		if hasAuth && user == requiredUser && password == requiredPassword {
			// Если аутентификация успешна - передаем запрос обработчику
			h(w, r, ps)
		} else {
			// Иначе запрашиваем аутентификацию
			w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
			http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
		}
	}
}

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprint(w, "Открытая зона!\n")
}

func Protected(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprint(w, "Защищенная зона!\n")
}

func main() {
	user := "gordon"
	pass := "secret!"

	router := httprouter.New()
	router.GET("/", Index)
	router.GET("/protected/", BasicAuth(Protected, user, pass))

	log.Fatal(http.ListenAndServe(":8080", router))
}

Цепочка обработчиков через NotFound

Примечание: Для корректной работы может потребоваться отключить Router.HandleMethodNotAllowed.

Вы можете использовать другой http.Handler (например, дополнительный роутер) для обработки запросов, которые не были найдены основным роутером:

router.NotFound = anotherRouter

Обслуживание статических файлов

Обработчик NotFound можно использовать для раздачи статических файлов из корневого пути /:

// Раздаем файлы из директории ./public
router.NotFound = http.FileServer(http.Dir("public"))

Однако такой подход нарушает строгие правила маршрутизации. Рекомендуется:

  1. Использовать выделенные подпути:
    router.ServeFiles("/static/*filepath", http.Dir("public"))
    
  2. Или явно задавать маршруты:
    router.GET("/files/*filepath", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
        http.ServeFile(w, r, path.Join("public", ps.ByName("filepath")))
    })
    

Веб-фреймворки на основе HttpRouter

Если HttpRouter кажется вам слишком минималистичным, рассмотрите эти высокоуровневые сторонние фреймворки, построенные на его основе:

Популярные решения

  1. Gin
    API в стиле Martini с значительно лучшей производительностью.
    Особенности: Middleware-цепочки, JSON-валидация, маршрутизация без рефлексии.

  2. Ace (устаревший, но исторически значимый)
    Один из первых быстрых фреймворков для Go.
    Преемник: Перешел в Gin.

  3. api2go
    Полноценная реализация JSON API с поддержкой JSON:API спецификации.
    Использование: Создание RESTful API с CRUD-операциями.

Для специфичных задач

  1. Goat
    Минималистичный REST API сервер.
    Философия: “Меньше кода — больше производительности”.

  2. Hitch
    Интегрирует HttpRouter с контекстом и middleware.
    Ключевая особенность: Простота связывания компонентов.

  3. Kami
    Работает через x/net/context.
    Для чего: Создание масштабируемых приложений с пробросом контекста.

  4. Siesta
    Композиция HTTP-обработчиков с поддержкой контекста.
    Паттерн: “Middleware как сервисы”.

Специализированные

  1. Medeina
    Вдохновлен Ruby-фреймворками Roda и Cuba.
    Подход: Древовидная маршрутизация.

  2. pbgo
    Мини-фреймворк для RPC/REST на основе Protobuf.
    Сценарии: Микросервисы с gRPC-like API.

  3. xmux
    Форк HttpRouter с поддержкой net/context.
    Отличие: Наследует производительность с расширенным API.

Для продакшена

  1. Hikaru
    Поддержка standalone-режима и Google App Engine.
    Плюсы: Кроссплатформенность.

  2. River
    Упрощенный REST-сервер для быстрого прототипирования.
    Фишка: Нулевая настройка для базовых сценариев.

  3. httpway
    Добавляет middleware-цепочки и graceful shutdown.
    Особенность: Совместимость с native HTTP-пакетами.


Как выбрать?

  • Для API: Gin, api2go, pbgo
  • Микросервисы: Siesta, xmux
  • Минимализм: Goat, River
  • Унаследованные проекты: Ace (переход на Gin)

Использование

Это всего лишь краткое введение, подробности смотрите в GoDoc. Давайте начнем с простого примера:

package main

import (
    "fmt"
    "net/http"
    "log"

    "github.com/julienschmidt/httprouter"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
    fmt.Fprint(w, "Welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    fmt.Fprintf(w, "hello, %s!\n", ps.ByName("name"))
}

func main() {
    router := httprouter.New()
    router.GET("/", Index)
    router.GET("/hello/:name", Hello)

    log.Fatal(http.ListenAndServe(":8080", router))
}

Принцип работы маршрутизации

Роутер сопоставляет входящие запросы по HTTP-методу и пути. Если для комбинации метод-путь зарегистрирован обработчик, запрос передается соответствующей функции. Для стандартных методов существуют сокращенные функции регистрации:

router.GET("/path", handler)    // GET
router.POST("/path", handler)   // POST 
router.PUT("/path", handler)    // PUT
router.PATCH("/path", handler)  // PATCH
router.DELETE("/path", handler) // DELETE

Для других методов используйте универсальный метод:

router.Handle("OPTIONS", "/path", handler)

Типы параметров пути

Синтаксис Тип
:name Именованный параметр
*name Catch-all параметр

1. Именованные параметры

Динамические сегменты пути. Соответствуют любому значению до следующего / или конца пути.

Шаблон:

$$/blog/:category/:post$$

Примеры соответствия:

Запрос Совпадение Параметры
/blog/go/request-routers category="go", post="request-routers"
/blog/go/request-routers/ ❌ (редирект) -
/blog/go/ -
/blog/go/request-routers/comments -

2. Catch-all параметры

Соответствуют любой части пути до конца, включая /. Всегда должны быть последним элементом.

Шаблон:

$$/files/*filepath$$

Примеры соответствия:

Запрос Совпадение Параметры
/files/ filepath="/"
/files/LICENSE filepath="/LICENSE"
/files/templates/article.html filepath="/templates/article.html"
/files ❌ (редирект) -

Работа с параметрами

Параметры хранятся в срезе структур Param (ключ-значение) и передаются обработчику третьим аргументом:

func Handler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    // Получение значения по имени
    user := ps.ByName("user") // для :user или *user

    // Получение по индексу (с доступом к имени параметра)
    param := ps[2]
    key   := param.Key   // имя 3-го параметра
    value := param.Value // значение 3-го параметра
}

Особенности:

  • Доступ к параметрам за O(1) по имени (использует внутреннюю оптимизацию)
  • Индексный доступ полезен при обработке неизвестных параметров
  • Параметры автоматически URL-decoded

Важные нюансы

  1. Редиректы:

    • Для путей с / в конце роутер автоматически делает редирект на каноничную версию
    • Можно отключить через router.RedirectTrailingSlash = false
  2. Кодировка:

    // Пример значения с спецсимволами
    // Запрос: /search/%D1%82%D0%B5%D1%81%D1%82
    query := ps.ByName("query") // автоматически декодируется в "тест"
    
  3. Производительность:

    • Параметры не аллоцируют память при отсутствии в пути
    • Используется slice pooling для повторного использования структур Param

Переменные

var ParamsKey = paramsKey{}

ParamsKey — это ключ контекста запроса, под которым хранятся параметры URL.


Функции

CleanPath

func CleanPath(p string) string

CleanPath — это URL-версия функции path.Clean. Она возвращает канонизированный путь, удаляя элементы . и ...

Правила обработки (применяются итеративно до полной обработки):

  1. Слэши
    Заменяет множественные слэши на один: ////

  2. Текущая директория (.)
    Удаляет элементы .:
    /././path/path

  3. Родительская директория (..)
    Удаляет комбинации ../ с предыдущим не-.. элементом:
    /a/b/../c/a/c
    /a/b/../../c/c

  4. Корневые ..
    Заменяет /.. в начале пути на /:
    /../a/a

  5. Пустой результат
    Если после обработки путь пуст, возвращает /:
    `` → /

Примеры:

CleanPath("//foo///bar")   // "/foo/bar"
CleanPath("/./foo/../bar") // "/bar"
CleanPath("/../")          // "/"
CleanPath("")              // "/"

Особенности:

  • Не выполняет URL-декодирование (работает с уже декодированным путем)
  • Сохраняет регистр символов
  • Полезен для нормализации путей перед маршрутизацией

Технические детали

  1. Использование в роутере:
    HttpRouter автоматически применяет CleanPath к входящим запросам перед сопоставлением маршрутов.

  2. Безопасность:
    Защищает от path traversal атак:

    // Запрос "/secret/../../etc/passwd" будет преобразован в "/etc/passwd",
    // но роутер обработает его только если есть явный маршрут
    
  3. Производительность:
    Реализация использует zero-allocation алгоритм для минимизации нагрузк

Типы

type Handle

type Handle func(http.ResponseWriter, *http.Request, Params)

Handle — функция, которая может быть зарегистрирована для обработки HTTP-запросов. Аналог http.HandlerFunc, но с третьим параметром для значений параметров URL.


type Param (добавлено в v1.1.0)

type Param struct {
    Key   string // Ключ параметра
    Value string // Значение параметра
}

Param представляет отдельный параметр URL, состоящий из ключа и значения.


type Params (добавлено в v1.1.0)

type Params []Param

Params — это срез параметров Param, возвращаемый роутером. Срез упорядочен: первый параметр URL соответствует первому элементу среза. Безопасен для доступа по индексу.

Методы:

  1. ParamsFromContext (добавлено в v1.2.0)
func ParamsFromContext(ctx context.Context) Params

Извлекает параметры URL из контекста запроса. Возвращает nil, если параметров нет.

  1. ByName (добавлено в v1.1.0)
func (ps Params) ByName(name string) string

Возвращает значение первого параметра с указанным ключом. Если параметр не найден, возвращает пустую строку.


type Router

type Router struct {
    // Автоматический редирект при несовпадении пути, 
    // но наличии обработчика для пути с/без завершающего слэша.
    // Пример: /foo/ → /foo (код 301 для GET, 307 для других методов)
    RedirectTrailingSlash bool

    // Автокоррекция пути при отсутствии обработчика:
    // 1. Удаляет избыточные элементы (../, //)
    // 2. Поиск без учета регистра
    // 3. Редирект на исправленный путь (301/307)
    RedirectFixedPath bool

    // Проверка допустимых методов при неудачной маршрутизации.
    // Если включено, отправляет 405 Method Not Allowed с заголовком Allow.
    HandleMethodNotAllowed bool

    // Автоматическая обработка OPTIONS-запросов.
    // Пользовательские обработчики OPTIONS имеют приоритет.
    HandleOPTIONS bool

    // Глобальный обработчик для автоматических OPTIONS-запросов.
    // Вызывается только если HandleOPTIONS=true и нет специфичного обработчика.
    GlobalOPTIONS http.Handler

    // Обработчик для ненайденных маршрутов (по умолчанию — http.NotFound).
    NotFound http.Handler

    // Обработчик для недопустимых методов (код 405).
    // По умолчанию — http.Error с StatusMethodNotAllowed.
    MethodNotAllowed http.Handler

    // Обработчик паник (код 500).
    // Предотвращает аварийное завершение сервера.
    PanicHandler func(http.ResponseWriter, *http.Request, interface{})

    // содержит скрытые или неэкспортируемые поля
}

Router реализует интерфейс http.Handler и предоставляет конфигурируемую систему маршрутизации.


Ключевые особенности

  1. Гибкость обработчиков:

    • Handle поддерживает параметры через Params
    • Совместимость со стандартными http.Handler/http.HandlerFunc
  2. Безопасность:

    • ParamsFromContext для безопасного доступа к параметрам из middleware
    • PanicHandler для обработки критических ошибок
  3. Производительность:

    • Срезы Params избегают аллокаций памяти
    • Прямой доступ к параметрам по индексу (O(1))

Пример использования параметров:

router.GET("/user/:id", func(w http.ResponseWriter, r *http.Request, ps Params) {
    id := ps.ByName("id") // или ps[0].Value
})

Методы типа Router

func New() *Router

func New() *Router

Создает и возвращает новый инициализированный роутер. Автокоррекция путей (включая обработку trailing slashes) включена по умолчанию.


Методы-сокращения для HTTP-методов

Метод Описание
DELETE(path string, handle Handle) Сокращение для router.Handle(http.MethodDelete, path, handle)
GET(path string, handle Handle) Сокращение для GET-запросов
HEAD(path string, handle Handle) (v1.1.0+) Сокращение для HEAD-запросов
OPTIONS(path string, handle Handle) (v1.1.0+) Сокращение для OPTIONS-запросов
PATCH(path string, handle Handle) Сокращение для PATCH-запросов
POST(path string, handle Handle) Сокращение для POST-запросов
PUT(path string, handle Handle) Сокращение для PUT-запросов
func (r *Router) DELETE(path string, handle Handle)
func (r *Router) GET(path string, handle Handle)

Пример:

router := httprouter.New()
router.GET("/users", listUsers)
router.POST("/users", createUser)

func (*Router) Handle(method, path string, handle Handle)

Регистрирует обработчик для указанного HTTP-метода и пути.

router.Handle("PROPFIND", "/resource", handleResource)

func (*Router) Handler(method, path string, handler http.Handler)

Адаптер для использования стандартного http.Handler. Параметры доступны через контекст:

router.Handler("GET", "/user/:id", customHandler)
// В обработчике:
params := httprouter.ParamsFromContext(r.Context())

func (*Router) HandlerFunc(method, path string, handler http.HandlerFunc)

Адаптер для http.HandlerFunc:

router.HandlerFunc("GET", "/", indexHandler)

func (*Router) Lookup(method, path string) (Handle, Params, bool)

Ручной поиск обработчика для комбинации метод+путь. Возвращает:

  • Обработчик
  • Параметры пути
  • Флаг необходимости редиректа (добавить/убрать trailing slash)

Пример использования в middleware:

if handle, ps, _ := router.Lookup(r.Method, r.URL.Path); handle != nil {
    // Кастомная обработка
}

func (*Router) ServeFiles(path string, root http.FileSystem)

Обслуживает статические файлы из указанной файловой системы. Путь должен содержать /*filepath в конце:

// Доступ к /var/www/file.txt по URL /src/file.txt
router.ServeFiles("/src/*filepath", http.Dir("/var/www"))

Особенности:

  • Использует стандартный http.FileServer
  • Для 404 ошибок применяется http.NotFound, а не роутерский NotFound-обработчик

func (*Router) ServeHTTP(w http.ResponseWriter, req *http.Request)

Позволяет роутеру удовлетворять интерфейсу http.Handler:

http.ListenAndServe(":8080", router)

Особенности использования

  1. Порядок регистрации:
    Нет приоритета “первый зарегистрированный — первый обработанный”. Каждый путь+метод соответствует ровно одному обработчику.

  2. Производительность:
    Все методы регистрации используют единую оптимизированную логику:

    // Эти вызовы эквивалентны по производительности
    router.GET("/path", handler)
    router.Handle("GET", "/path", handler)
    
  3. Безопасность:
    ServeFiles автоматически защищает от directory traversal атак:

    // Запрос `/src/../../../etc/passwd` будет отклонен