Начало

Обработка HTTP-запросов с помощью Go в основном сводится к двум вещам: handlers (обработчикам) и servermuxes (серверным мультиплексорам).

Если вы имеете опыт работы с MVC, то можете представить себе обработчики как нечто похожее на контроллеры. В целом, они отвечают за выполнение логики вашего приложения и запись заголовков и тел ответов.

В то время как servemux (также известный как маршрутизатор (router)) хранит сопоставление между предопределенными URL-путями для вашего приложения и соответствующими обработчиками. Обычно у вас есть один servemux для вашего приложения, содержащий все ваши маршруты.

Пакет net/http языка Go поставляется с простым, но эффективным сервером http.ServeMux, а также несколькими функциями для генерации общих обработчиков, включая http.FileServer(), http.NotFoundHandler() и http.RedirectHandler().

Давайте рассмотрим простой (но немного надуманный!) пример, в котором используются эти элементы:

CONSOLE
$ mkdir example
$ cd example
$ go mod init example.com
$ touch main.go
Нажмите, чтобы развернуть и увидеть больше
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}
Нажмите, чтобы развернуть и увидеть больше

Давайте запустим приложение:

CONSOLE
$ go run main.go
2025/08/25 11:09:43 Listening on port 3000
Нажмите, чтобы развернуть и увидеть больше

И если вы отправите запрос на http://localhost:3000/foo, вы увидите, что он успешно перенаправится следующим образом:

CONSOLE
$ 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.

CONSOLE
$ 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, который выглядит следующим образом:

GO
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
Нажмите, чтобы развернуть и увидеть больше

Если вы не знакомы с интерфейсами в Go, я написал объяснение здесь, но проще говоря, это означает, что обработчик должен иметь метод ServeHTTP() со следующей сигнатурой:

GO
ServeHTTP(http.ResponseWriter, *http.Request)
Нажмите, чтобы развернуть и увидеть больше

Для наглядности давайте создадим пользовательский обработчик, который отвечает текущим временем в определенном формате. Например, так:

GO
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). Это все, что нам нужно для создания обработчика.

Давайте попробуем это на конкретном примере:

main.go
 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. Вы должны получить ответ, содержащий текущее время, примерно такой:

CONSOLE
$ curl localhost:3000/time
Текущее время: 2025-08-25T12:16:50+03:00
Нажмите, чтобы развернуть и увидеть больше

Давайте разберемся, что здесь происходит:

  1. Когда наш Go-сервер получает входящий HTTP-запрос, он передает его нашему servemux (тому, который мы передали функции http.ListenAndServe()).
  2. Затем servemux ищет соответствующий обработчик на основе пути запроса (в данном случае путь /time сопоставляется с нашим обработчиком timeHandler).
  3. Затем серверный мультиплексор вызывает метод ServeHTTP() обработчика, который, в свою очередь, записывает HTTP-ответ.

Внимательные читатели, возможно, заметили еще одну интересную деталь: сигнатура функции http.ListenAndServe() выглядит так: ListenAndServe(addr string, handler Handler), но мы передали servemux в качестве второго аргумента.

Мы смогли это сделать, потому что тип http.ServeMux имеет метод ServeHTTP(), что означает, что он также удовлетворяет интерфейсу http.Handler.

Для меня проще думать о http.ServeMux как об особом типе обработчика, который вместо того, чтобы сам предоставлял ответ, передает запрос второму обработчику. Это не так сложно, как кажется на первый взгляд — цепочки обработчиков очень распространены в Go.

Функции в качестве обработчиков

В простых случаях (как в примере выше) определение нового пользовательского типа только для создания обработчика кажется немного излишним. К счастью, мы можем переписать обработчик в виде простой функции:

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.

Давайте воспроизведем наше приложение с помощью этой техники:

main.go
 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(). Вы можете использовать его следующим образом:

GO
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() в обработчик?

Хороший подход заключается в том, чтобы поместить логику нашего обработчика в замыкание и закрыть переменные, которые мы хотим использовать, например так:

main.go
 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() возвращает это преобразованное замыкание.

В этом примере мы просто передали простую строку в обработчик. Но в реальном приложении вы можете использовать этот метод для передачи подключения к базе данных, карты шаблонов или любого другого контекста на уровне приложения. Это хорошая альтернатива использованию глобальных переменных, а также имеет дополнительное преимущество в виде создания аккуратных автономных обработчиков для тестирования.

Вы также можете увидеть этот же паттерн, записанный следующим образом:

GO
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 при возвращении:

GO
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:

GO
var DefaultServeMux = NewServeMux()
Нажмите, чтобы развернуть и увидеть больше

Вместо этого лучше использовать собственный серверный мультиплексор с локальной областью действия, как мы делали до сих пор. Но если вы все же решите использовать сервермукс по умолчанию…

Пакет net/http предоставляет несколько ярлыков для регистрации маршрутов с помощью servermux по умолчанию: http.Handle() и http.HandleFunc(). Они выполняют точно те же функции, что и одноименные функции, которые мы уже рассмотрели, с той разницей, что они добавляют обработчики к servermux по умолчанию, а не к созданному вами.

Кроме того, http.ListenAndServe() будет использовать серверный мультиплексор по умолчанию, если не указан другой обработчик (то есть второй аргумент установлен в nil).

Итак, в качестве последнего шага давайте продемонстрируем, как вместо этого использовать в нашем приложении серверный мультиплексор по умолчанию:

main.go
 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}
Нажмите, чтобы развернуть и увидеть больше

Авторское право

Автор: Арсений Соколов

Ссылка: https://cdn.arsen.pw/posts/an-introduction-to-handlers-and-servemuxes-in-go/

Лицензия: CC BY-NC-SA 4.0

Эта работа лицензирована в соответствии с международной лицензией Creative Commons Attribution-NonCommercial-ShareAlike 4.0. Пожалуйста, указывайте источник, используйте в некоммерческих целях и сохраняйте ту же лицензию.

Комментарии

Начать поиск

Введите ключевые слова для поиска статей

↑↓
ESC
⌘K Горячая клавиша