Сервер в несколько строк
Обработчик это функция, которая получает запрос и пишет ответ. 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.Method | GET, 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}" | GET (и HEAD) на /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).
Коды статуса и порядок записи
У ответа три части, и писать их нужно по порядку: заголовки, статус, тело.
w.Header().Set(...)меняет заголовки. Это действует, только пока статус не отправлен.w.WriteHeader(code)отправляет строку статуса и заголовки.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).