Начало
Обработка HTTP-запросов с помощью Go в основном сводится к двум вещам: handlers (обработчикам) и servermuxes (серверным мультиплексорам).
Если вы имеете опыт работы с MVC, то можете представить себе обработчики как нечто похожее на контроллеры. В целом, они отвечают за выполнение логики вашего приложения и запись заголовков и тел ответов.
В то время как servemux (также известный как маршрутизатор (router)) хранит сопоставление между предопределенными URL-путями для вашего приложения и соответствующими обработчиками. Обычно у вас есть один servemux для вашего приложения, содержащий все ваши маршруты.
Пакет net/http
языка Go поставляется с простым, но эффективным сервером http.ServeMux, а также несколькими функциями для генерации общих обработчиков, включая http.FileServer(), http.NotFoundHandler() и http.RedirectHandler().
Давайте рассмотрим простой (но немного надуманный!) пример, в котором используются эти элементы:
$ mkdir example
$ cd example
$ go mod init example.com
$ touch main.go
1package main
2
3import (
4 "log"
5 "net/http"
6)
7
8func main() {
9 // Используйте функцию http.NewServeMux() для создания пустого сервера мультиплексора.
10 mux := http.NewServeMux()
11
12 // Используйте функцию http.RedirectHandler(), чтобы создать обработчик,
13 // который перенаправляет все полученные запросы на http://example.org с кодом 307.
14 rh := http.RedirectHandler("http://example.org", http.StatusTemporaryRedirect)
15
16 // Далее мы используем функцию mux.Handle(), чтобы зарегистрировать его
17 // в нашем новом servemux, чтобы он действовал как обработчик всех входящих запросов
18 // с URL-путем /foo.
19 mux.Handle("/foo", rh)
20
21 log.Print("Listening on port 3000")
22
23 // Затем мы создаем новый сервер и начинаем прослушивать входящие запросы
24 // с помощью функции http.ListenAndServe(), передавая в качестве второго аргумента
25 // наш servemux для сопоставления запросов.
26 http.ListenAndServe(":3000", mux)
27}
Давайте запустим приложение:
$ go run main.go
2025/08/25 11:09:43 Listening on port 3000
И если вы отправите запрос на http://localhost:3000/foo
, вы увидите, что он успешно перенаправится следующим образом:
$ curl -IL localhost:3000/foo
HTTP/1.1 307 Temporary Redirect
Content-Type: text/html; charset=utf-8
Location: http://example.com
Date: Mon, 25 Aug 2025 08:51:11 GMT
HTTP/1.1 200 OK
Content-Type: text/html
ETag: "84238dfc8092e5d9c0dac8ef93371a07:1736799080.121134"
Last-Modified: Mon, 13 Jan 2025 20:11:20 GMT
Cache-Control: max-age=372
Date: Mon, 25 Aug 2025 08:51:12 GMT
Connection: keep-alive
В то время как все остальные запросы должны получать ответ с ошибкой 404 Not Found
.
$ curl -IL localhost:3000/bar
HTTP/1.1 404 Not Found
Content-Type: text/plain; charset=utf-8
X-Content-Type-Options: nosniff
Date: Mon, 25 Aug 2025 08:55:27 GMT
Content-Length: 19
Пользовательские обработчики
Обработчики, поставляемые с net/http, полезны, но в большинстве случаев при создании веб-приложения вы захотите использовать свои собственные настраиваемые обработчики. Как это сделать?
Первое, что нужно объяснить, это то, что в Go все может быть обработчиком, если оно удовлетворяет интерфейсу http.Handler, который выглядит следующим образом:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Если вы не знакомы с интерфейсами в Go, я написал объяснение здесь, но проще говоря, это означает, что обработчик должен иметь метод ServeHTTP()
со следующей сигнатурой:
ServeHTTP(http.ResponseWriter, *http.Request)
Для наглядности давайте создадим пользовательский обработчик, который отвечает текущим временем в определенном формате. Например, так:
type timeHandler struct {
format string
}
func (th timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
tm := time.Now().Format(th.format)
w.Write([]byte("Текущее время: " + tm))
}
Точный код здесь не слишком важен.
Все, что действительно имеет значение, это то, что у нас есть объект (в данном случае это структура timeHandler
, но это может быть и строка, и функция, и что-либо еще), и мы реализовали для него метод с сигнатурой ServeHTTP(http.ResponseWriter, *http.Request)
. Это все, что нам нужно для создания обработчика.
Давайте попробуем это на конкретном примере:
1package main
2
3import (
4 "log"
5 "net/http"
6 "time"
7)
8
9type timeHandler struct {
10 format string
11}
12
13func (th timeHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
14 tm := time.Now().Format(th.format)
15 w.Write([]byte("Текущее время: " + tm))
16}
17
18func main() {
19 mux := http.NewServeMux()
20
21 // Инициализируйте timeHandler точно так же, как любую обычную структуру.
22 th := timeHandler{format: time.RFC3339}
23
24 // Как и в предыдущем примере, мы используем функцию mux.Handle(),
25 // чтобы зарегистрировать его в нашем ServeMux.
26 mux.Handle("/time", th)
27
28 log.Println("Listening on port 3000")
29 http.ListenAndServe(":3000", mux)
30}
Запустите приложение, а затем попробуйте отправить запрос на http://localhost:3000/time
. Вы должны получить ответ, содержащий текущее время, примерно такой:
$ curl localhost:3000/time
Текущее время: 2025-08-25T12:16:50+03:00
Давайте разберемся, что здесь происходит:
- Когда наш Go-сервер получает входящий HTTP-запрос, он передает его нашему servemux (тому, который мы передали функции
http.ListenAndServe()
). - Затем servemux ищет соответствующий обработчик на основе пути запроса (в данном случае путь
/time
сопоставляется с нашим обработчикомtimeHandler
). - Затем серверный мультиплексор вызывает метод
ServeHTTP()
обработчика, который, в свою очередь, записывает HTTP-ответ.
Внимательные читатели, возможно, заметили еще одну интересную деталь: сигнатура функции http.ListenAndServe()
выглядит так: ListenAndServe(addr string, handler Handler)
, но мы передали servemux в качестве второго аргумента.
Мы смогли это сделать, потому что тип http.ServeMux
имеет метод ServeHTTP(), что означает, что он также удовлетворяет интерфейсу http.Handler
.
Для меня проще думать о http.ServeMux
как об особом типе обработчика, который вместо того, чтобы сам предоставлял ответ, передает запрос второму обработчику. Это не так сложно, как кажется на первый взгляд — цепочки обработчиков очень распространены в Go.
Функции в качестве обработчиков
В простых случаях (как в примере выше) определение нового пользовательского типа только для создания обработчика кажется немного излишним. К счастью, мы можем переписать обработчик в виде простой функции:
func timeHandler(w http.ResponseWriter, r *http.Request) {
tm := time.Now().Format(time.RFC3339)
w.Write([]byte("Текущее время: " + tm))
}
Теперь, если вы следили за объяснением, вы, вероятно, смотрите на всё это и задаетесь вопросом: “Как это может быть обработчиком? У него нет метода ServeHTTP()
.”
И вы будете правы. Эта функция сама по себе не является обработчиком. Но мы можем заставить ее стать обработчиком, преобразовав ее в тип http.HandlerFunc.
В принципе, любая функция, имеющая сигнатуру func(http.ResponseWriter, *http.Request)
, может быть преобразована в тип http.HandlerFunc
. Это полезно, поскольку объекты http.HandlerFunc
имеют встроенный метод ServeHTTP()
, который — довольно умно и удобно — выполняет содержимое исходной функции.
Если это звучит непонятно, попробуйте взглянуть на соответствующий исходный код. Вы увидите, что это очень лаконичный способ сделать функцию совместимой с интерфейсом http.Handler
.
Давайте воспроизведем наше приложение с помощью этой техники:
1package main
2
3import (
4 "log"
5 "net/http"
6 "time"
7)
8
9func timeHandler(w http.ResponseWriter, r *http.Request) {
10 tm := time.Now().Format(time.RFC3339)
11 w.Write([]byte("Текущее время: " + tm))
12}
13
14func main() {
15 mux := http.NewServeMux()
16
17 // Преобразуйте функцию timeHandler в тип http.HandlerFunc.
18 th := http.HandlerFunc(timeHandler)
19
20 // И добавьте его в ServeMux.
21 mux.HandleFunc("/time", th)
22
23 log.Println("Listening on port 3000")
24 http.ListenAndServe(":3000", mux)
25}
На самом деле, преобразование функции в тип http.HandlerFunc
, а затем добавление ее в servemux таким образом настолько распространено, что Go предоставляет для этого специальный метод: mux.HandleFunc(). Вы можете использовать его следующим образом:
func main() {
mux := http.NewServeMux()
th := http.HandlerFunc(timeHandler)
mux.HandleFunc("/time", th)
log.Println("Listening on port 3000")
http.ListenAndServe(":3000", mux)
}
Передача переменных обработчикам
В большинстве случаев использование функции в качестве обработчика работает хорошо. Но когда дела становятся более сложными, возникают некоторые ограничения.
Вы, наверное, заметили, что, в отличие от предыдущего метода, нам пришлось жестко прописать формат времени в функции timeHandler
. Что произойдет, если вы захотите передать информацию или переменные из main()
в обработчик?
Хороший подход заключается в том, чтобы поместить логику нашего обработчика в замыкание и закрыть переменные, которые мы хотим использовать, например так:
1package main
2
3import (
4 "log"
5 "net/http"
6 "time"
7)
8
9func timeHandler(format string) http.Handler {
10 fn := func(w http.ResponseWriter, r *http.Request) {
11 tm := time.Now().Format(format)
12 w.Write([]byte("Текущее время: " + tm))
13 }
14 return http.HandlerFunc(fn)
15}
16
17func main() {
18 mux := http.NewServeMux()
19
20 th := timeHandler(time.RFC3339)
21 mux.Handle("/time", th)
22
23 log.Println("Listening on port 3000")
24 http.ListenAndServe(":3000", mux)
25}
Функция timeHandler()
теперь выполняет немного иную роль. Вместо того, чтобы принудительно преобразовывать функцию в обработчик (как мы делали ранее), мы теперь используем ее для возврата обработчика. Для этого необходимо два ключевых элемента.
Сначала создается fn
, анонимная функция, которая обращается к переменной format
или закрывает ее, образуя замыкание. Независимо от того, что мы делаем с замыканием, оно всегда будет иметь доступ к переменным, которые являются локальными для области, в которой оно было создано — в данном случае это означает, что оно всегда будет иметь доступ к переменной format
.
Во-вторых, наше замыкание имеет сигнатуру func(http.ResponseWriter, *http.Request)
. Как вы, возможно, помните из предыдущего раздела, это означает, что мы можем преобразовать его в тип http.HandlerFunc
(чтобы он удовлетворял интерфейсу http.Handler
). Затем наша функция timeHandler()
возвращает это преобразованное замыкание.
В этом примере мы просто передали простую строку в обработчик. Но в реальном приложении вы можете использовать этот метод для передачи подключения к базе данных, карты шаблонов или любого другого контекста на уровне приложения. Это хорошая альтернатива использованию глобальных переменных, а также имеет дополнительное преимущество в виде создания аккуратных автономных обработчиков для тестирования.
Вы также можете увидеть этот же паттерн, записанный следующим образом:
func timeHandler(format string) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tm := time.Now().Format(format)
w.Write([]byte("Текущее время: " + tm))
})
}
Или с помощью неявного преобразования в тип http.HandlerFunc
при возвращении:
func timeHandler(format string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
tm := time.Now().Format(format)
w.Write([]byte("Текущее время: " + tm))
}
}
Служба servemux по умолчанию
Вы, вероятно, видели упоминания о стандартном servemux во многих местах, от простейших примеров Hello World до исходного кода Go.
Мне потребовалось много времени, чтобы понять, что в этом нет ничего особенного. Сервемукс по умолчанию — это обычный сервемукс, который мы уже использовали, который создается по умолчанию при использовании пакета net/http
и хранится в глобальной переменной. Вот соответствующая строка из исходного кода Go:
var DefaultServeMux = NewServeMux()
Предупреждение
В целом, я не рекомендую использовать сервермукс по умолчанию, поскольку он делает ваш код менее понятным и явным, а также создает угрозу безопасности. Поскольку он хранится в глобальной переменной, любой пакет может получить к нему доступ и зарегистрировать маршрут, включая любые сторонние пакеты, которые импортирует ваше приложение. Если один из этих сторонних пакетов будет взломан, злоумышленники смогут использовать серверный мультиплексор по умолчанию для размещения вредоносного обработчика в сети.
Вместо этого лучше использовать собственный серверный мультиплексор с локальной областью действия, как мы делали до сих пор. Но если вы все же решите использовать сервермукс по умолчанию…
Пакет net/http
предоставляет несколько ярлыков для регистрации маршрутов с помощью servermux по умолчанию: http.Handle() и http.HandleFunc(). Они выполняют точно те же функции, что и одноименные функции, которые мы уже рассмотрели, с той разницей, что они добавляют обработчики к servermux по умолчанию, а не к созданному вами.
Кроме того, http.ListenAndServe()
будет использовать серверный мультиплексор по умолчанию, если не указан другой обработчик (то есть второй аргумент установлен в nil).
Итак, в качестве последнего шага давайте продемонстрируем, как вместо этого использовать в нашем приложении серверный мультиплексор по умолчанию:
1package main
2
3import (
4 "log"
5 "net/http"
6 "time"
7)
8
9func timeHandler(format string) http.Handler {
10 fn := func(w http.ResponseWriter, r *http.Request) {
11 tm := time.Now().Format(format)
12 w.Write([]byte("Текущее время: " + tm))
13 }
14 return http.HandlerFunc(fn)
15}
16
17func main() {
18 // Обратите внимание, что мы пропускаем создание ServeMux...
19
20 var format = time.RFC3339
21 th := timeHandler(format)
22
23 // Мы используем http.Handle вместо mux.Handle...
24 http.Handle("/time", th)
25
26 log.Println("Listening on port 3000")
27 // И передайте nil в качестве обработчика в ListenAndServe.
28 http.ListenAndServe(":3000", nil)
29}
Комментарии