Menu

HTTP-сервер на Golang: маршрутизация net/http, JSON и статусы

Как построить веб-сервер на стандартном пакете net/http: обработчики, маршрутизация ServeMux с методами и шаблонами в пути (Go 1.22), JSON-ответы, коды статуса, middleware, таймауты и корректное завершение.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Сервер в несколько строк

Обработчик это функция, которая получает запрос и пишет ответ. ServeMux направляет запросы к обработчикам.

Редактор не может принимать соединения от вашего браузера, поэтому примеры на этой странице запускают сервер через httptest.NewServer и обращаются к нему из той же программы. В настоящей программе последняя часть заменяется одной строкой, которая блокируется и обслуживает запросы вечно:

log.Fatal(http.ListenAndServe(":8080", mux))

Тогда curl localhost:8080/hello/gopher напечатает Hello, gopher!. ListenAndServe возвращается только при ошибке (например, порт занят), поэтому его оборачивают в log.Fatal.

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

Обработчики

Всё, у чего есть метод ServeHTTP(http.ResponseWriter, *http.Request), является http.Handler. http.HandlerFunc приспосабливает обычную функцию к этому интерфейсу, а mux.HandleFunc делает это преобразование за вас. Обработчик-структура удобен, когда обработчикам нужны зависимости:

type API struct {
	db *sql.DB
}

func (a *API) listItems(w http.ResponseWriter, r *http.Request) { /* uses a.db */ }

mux.HandleFunc("GET /items", api.listItems)

*http.Request даёт вам:

Поле или методСодержит
r.MethodGET, POST, ...
r.URL.Pathпуть, /items/42
r.PathValue("id")подстановку из шаблона маршрута (Go 1.22)
r.URL.Query().Get("q")параметр строки запроса
r.Header.Get("Authorization")заголовок запроса
r.Bodyтело запроса, io.ReadCloser (его закрывает сервер)
r.FormValue("name")поле формы или параметр запроса
r.Context()контекст, который отменяется, когда клиент отключается

Шаблоны маршрутов (Go 1.22)

Начиная с Go 1.22 шаблоны ServeMux имеют вид [METHOD ][HOST]/[PATH], а пути могут содержать подстановки.

ШаблонСовпадает с
"/items/"/items/ и всем, что под ним (слэш в конце означает префикс)
"/items"только /items
"GET /items/{id}"GETHEAD) на /items/42; r.PathValue("id") == "42"
"POST /items"только POST на /items
"/files/{path...}"/files/a/b/c; path равно "a/b/c"
"/{$}"только /, а не любой путь
"/"любой путь, с которым не совпал ни один другой шаблон

Когда совпадают два шаблона, побеждает более конкретный, так что /items/new побеждает /items/{id}. Если ни один не конкретнее, например /items/{id} и /{kind}/new (оба совпадают с /items/new), регистрация второго вызывает панику с сообщением, где названы оба шаблона. Если путь совпал, а метод нет, мультиплексор отвечает 405 Method Not Allowed с заголовком Allow, без всякого кода с вашей стороны.

Шаблон "/" ловит всё. Это удивляет тех, кто регистрирует главную страницу на "/" и обнаруживает, что она отвечает 200 на любой неизвестный URL. Для главной страницы используйте "GET /{$}".

Этим шаблонам нужно go 1.22 или новее в go.mod. При более старой строке версии мультиплексор возвращается к старому поведению: он читает "GET /items" как имя хоста, за которым идёт путь, так что маршрут никогда не совпадает, и каждый запрос к /items получает 404.

Небольшой JSON API

Последний запрос использует DELETE, который не принимает ни один маршрут, и мультиплексор сам отвечает 405 с Allow: GET, HEAD: это методы, зарегистрированные для этого пути (маршрут GET принимает и HEAD).

Коды статуса и порядок записи

У ответа три части, и писать их нужно по порядку: заголовки, статус, тело.

  1. w.Header().Set(...) меняет заголовки. Это действует, только пока статус не отправлен.
  2. w.WriteHeader(code) отправляет строку статуса и заголовки.
  3. w.Write(...) (или fmt.Fprint(w, ...), или кодировщик) отправляет тело. Если WriteHeader не вызывали, первый Write сначала отправит 200 OK.

Следствия:

  • Заголовок, заданный после начала тела, ни на что не влияет.
  • Повторный вызов WriteHeader пишет в лог http: superfluous response.WriteHeader call и оставляет первый статус.
  • После ответа с ошибкой делайте return. http.Error не останавливает ваш обработчик, и код после него продолжит писать в тот же ответ.

Используйте именованные константы (http.StatusOK, http.StatusCreated, http.StatusBadRequest, http.StatusUnauthorized, http.StatusNotFound, http.StatusInternalServerError), а не голые числа. http.StatusText(404) возвращает "Not Found".

Middleware

Middleware это функция, которая принимает обработчик и возвращает обработчик, делая что-то до или после вызова следующего:

Для одного и того же запроса строка log: всегда появляется раньше client got:. Горутина обработчика печатает её до возврата, а сервер не завершает ответ, пока обработчик не вернётся.

statusRecorder встраивает настоящий ResponseWriter и переопределяет только WriteHeader; это стандартный способ узнать код статуса из middleware.

Настройки для продакшена

http.ListenAndServe использует сервер без таймаутов, так что медленный клиент может держать соединение открытым сколько угодно. Для всего, что смотрит в интернет, настраивайте http.Server явно и завершайте его корректно:

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

go func() {
	if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Fatal(err)
	}
}()

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
<-ctx.Done() // wait for Ctrl+C or a SIGTERM from the orchestrator

shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil { // finish in-flight requests
	log.Println("shutdown:", err)
}

Shutdown перестаёт принимать новые соединения и ждёт завершения активных запросов, но не дольше дедлайна контекста. ListenAndServe возвращает http.ErrServerClosed, как только начинается Shutdown, поэтому эта ошибка не считается сбоем. Долгим обработчикам передавайте r.Context(), чтобы они останавливались, когда клиент уходит.

Раздача статических файлов занимает одну строку: mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("public")))).

Частые ошибки

  • Нет return после http.Error. Обработчик продолжает работать и дописывает в ответ.
  • Заголовки после записи тела. Они молча отбрасываются.
  • Главная страница на "/". Она ловит все неизвестные пути. Используйте "/{$}".
  • Общее состояние обработчиков без блокировки. Запросы выполняются конкурентно.
  • Сервер по умолчанию в продакшене. У него нет таймаутов; задайте их в http.Server.
  • Шаблоны маршрутов игнорируются. Если "GET /x/{id}" никогда не совпадает, проверьте, что в go.mod указано go 1.22 или новее.

Часто задаваемые вопросы

Как создать простой веб-сервер на Go?

Зарегистрируйте обработчик и начните слушать порт: http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "hello") }), затем log.Fatal(http.ListenAndServe(":8080", nil)). Сервер из стандартной библиотеки годится для продакшена: он поддерживает HTTP/1.1, HTTP/2 поверх TLS, keep-alive и запускает по горутине на соединение.

Как получить параметр пути в net/http в Go?

Начиная с Go 1.22 шаблоны ServeMux могут содержать подстановки: зарегистрируйте mux.HandleFunc("GET /items/{id}", h) и прочитайте значение в обработчике через r.PathValue("id"). Завершающий {path...} совпадает с остатком пути. До 1.22 приходилось разбирать r.URL.Path вручную или брать маршрутизатор вроде chi.

Нужен ли фреймворк вроде Gin, чтобы сделать REST API на Go?

Нет. Начиная с Go 1.22 net/http маршрутизирует по методу и параметрам пути, а это и была главная причина использовать маршрутизаторы. С encoding/json для тел запросов и небольшими функциями middleware для логирования и аутентификации стандартной библиотеки хватает для большинства API. Фреймворки добавляют удобства вроде привязки запросов и валидации.

Как задать код статуса в HTTP-обработчике на Go?

Вызовите w.WriteHeader(http.StatusCreated) до записи тела. Если сначала записать тело, Go автоматически отправит 200 OK, а последующий WriteHeader будет проигнорирован с сообщением в логе superfluous response.WriteHeader call. Заголовки тоже задавайте через w.Header().Set до WriteHeader; для ошибок всё это делает http.Error(w, msg, code).

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ