HTTP-сервер в Go: что net/http делает сам и чего не делает, пока не попросят
Собеседование по HTTP-серверу идёт лестницей: как сделать веб-сервер — как он обслуживает конкурентные запросы — какие таймауты задать — что делать, когда клиент ушёл, — как остановить сервер, не оборвав запросы. Прогон отвечает на каждый шаг: на каждое соединение — своя горутина и никакого встроенного предела; у http.Server{} все четыре таймаута нулевые, и молчащий клиент держит соединение сколько угодно; контекст запроса отменяется, а работа — нет; и Serve возвращается сразу после вызова Shutdown, пока запросы ещё в полёте.
Полное техническое изложение
TL;DR
HTTP-сервер в Go — это http.Handler и http.Server. Обработчик — любой
тип с методом ServeHTTP(w, r); ServeMux раскладывает запросы по
обработчикам; ListenAndServe слушает порт. Сервер готов в пять строк, и
ровно поэтому вопросы на собеседовании начинаются после этих пяти строк.
Сервер конкурентен сам: на каждое соединение — своя горутина, а в HTTP/2 —
на каждый поток запроса внутри соединения. Десять запросов на десяти
соединениях к обработчику, который спит 200 мс, заняли 209 мс, а не две
секунды. Отсюда первое следствие: изменяемое состояние, общее для
обработчиков, требует синхронизации. И второе: фиксированного предела на
число одновременных запросов net/http не ставит — ни пула обработчиков,
ни таймаутов по умолчанию.
У http.Server{} все четыре таймаута нулевые, а ноль значит «не ограничено»:
клиент, приславший начало заголовков и замолчавший, через 2 с всё ещё держал
соединение; с ReadHeaderTimeout 300 мс сервер закрыл его через 301 мс.
Дальше — то, что отличает понимание от пересказа. Когда клиент ушёл,
r.Context() отменяется, но работу останавливает только тот обработчик,
который этот контекст слушает: второй доделал свои 500 мс. Shutdown
дожидается запросов в полёте, Close их обрывает — но ListenAndServe
возвращается сразу после вызова Shutdown, через 0 мс, пока запросы ещё
идут. Программа, которая на этом и завершается, обрывает их так же, как
Close.
- что такое горутина и интерфейс;
- как работает отмена через
context.Context— это урок о context; - что такое HTTP-запрос, заголовки и код ответа.
http.Handler,ServeMux, промежуточный слой;- горутина на соединение, четыре таймаута сервера, контекст запроса;
ShutdownпротивClose, паника в обработчике, шаблоныServeMuxGo 1.22.
Что здесь на самом деле спрашивают
Лестница почти всегда такая:
- «Как сделать веб-сервер на Go?» — разминка:
http.HandleFuncиListenAndServe. - «Что такое
http.Handlerи промежуточный слой?» — проверяют, видит ли кандидат за функцией интерфейс. - «Как
net/httpобслуживает конкурентные запросы?» — вопрос, ответ на который объясняет половину остальных. - «Какие таймауты задать серверу?» — здесь видно, был ли у кандидата сервер в продакшене.
- «Что будет с работой, если клиент ушёл?» — про контекст запроса.
- «Как остановить сервер, не оборвав запросы?» — graceful shutdown, и ловушка в нём.
Стержень у лестницы один: net/http по умолчанию ничего не ограничивает и
ничего не дожидается сам. Каждое «ограничить» и «дождаться» — строка, которую
пишет разработчик, и вопросы собеседования — ровно про эти строки.
База: Handler, ServeMux и промежуточный слой
Всё в net/http держится на одном интерфейсе:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}http.HandlerFunc превращает обычную функцию в Handler, ServeMux — тоже
Handler, который выбирает другой обработчик по пути, а http.Server берёт
один Handler и обслуживает им соединения. Отсюда промежуточный слой
(middleware) — функция, которая принимает Handler и возвращает Handler,
обернув вызов своим кодом:
func logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}Ответ уровня Junior на «как сделать веб-сервер»: объявить обработчики —
функции func(w http.ResponseWriter, r *http.Request) — зарегистрировать их в
http.ServeMux и запустить http.Server с этим мультиплексором. Обработчик —
это интерфейс http.Handler, поэтому промежуточный слой пишется как функция
Handler → Handler. И сразу оговорка, которую стоит произнести: в
продакшене не http.ListenAndServe(addr, mux), а свой http.Server — потому
что только у него есть поля таймаутов и метод Shutdown.
Механизм 1: горутина на соединение — и никакого встроенного предела
Serve accepts incoming HTTP connections on the listener l, creating a new
service goroutine for each.
Serve принимает входящие HTTP-соединения на слушателе l и создаёт для каждого отдельную обслуживающую горутину.
Десять клиентов, каждый на своём соединении, к обработчику, который спит 200 мс:
1) ГОРУТИНА НА СОЕДИНЕНИЕ
─────────────────────────
10 запросов, обработчик спит 200 мс: всего 209 мс
обработчиков одновременно, пик: 10
В этом замере каждый запрос шёл по своему соединению. Но соединение — не запрос, и модель «один запрос = одно соединение = одна горутина» неверна в обе стороны:
1) HTTP/1.1 KEEP-ALIVE: ЗАПРОСЫ ПОДРЯД
──────────────────────────────────────
10 запросов подряд одним клиентом: соединений у сервера 1
2) HTTP/2: ОДНОВРЕМЕННЫЕ ЗАПРОСЫ
────────────────────────────────
10 одновременных запросов: 202 мс, новых соединений 0, обработчиков одновременно 10, ответ 200 HTTP/2.0
В HTTP/1.1 с keep-alive одно соединение обслуживает запросы по очереди. В
HTTP/2 по одному соединению идут одновременные потоки, и горутина
обработчика — у каждого потока (прогон bench/gohttp/protocols.go, HTTP/2 без
TLS через Server.Protocols go1.24).
Из этого следуют три вещи, которые стоит уметь сказать.
- Обработчики выполняются одновременно. Изменяемое состояние, к которому обработчики обращаются конкурентно и хотя бы один пишет, без мьютекса или атомарных операций — гонка данных (урок о гонках данных). Неизменяемое состояние — конфигурацию, прочитанную при старте, — можно делить свободно.
- Медленный обработчик сам по себе не задерживает другие соединения. Но общий мьютекс, пул соединений к базе или семафор связывают задержки запросов: кто ждёт общий ресурс, тот ждёт и медленного соседа.
- Встроенного предела на число запросов нет.
net/httpне держит пула обработчиков с фиксированным размером. Пределы есть, но другие: дескрипторы и очередьaccept, память, у HTTP/2 — число одновременных потоков на соединение (по документацииHTTP2Config— не меньше 100 по умолчанию), балансировщик перед сервером. Ограничивают явно — таймаутами (Механизм 2) или семафором в промежуточном слое.
Механизм 2: четыре таймаута, и все нулевые
У http.Server четыре поля таймаутов, и по умолчанию все — ноль:
A zero or negative value means there will be no timeout.
Нулевое или отрицательное значение означает, что таймаута не будет.
Что это значит на деле: клиент открыл соединение, прислал начало заголовков и замолчал.
2) ТАЙМАУТЫ СЕРВЕРА ПО УМОЛЧАНИЮ
────────────────────────────────
http.Server{}: ReadTimeout=0s ReadHeaderTimeout=0s WriteTimeout=0s IdleTimeout=0s
клиент прислал начало заголовков и замолчал:
ReadHeaderTimeout=0s через 2000 мс: соединение всё ещё открыто
ReadHeaderTimeout=300ms через 301 мс: соединение закрыто сервером
2 с — предел ожидания самого клиента в замере; сервер без таймаута ждал бы дольше. Каждое такое соединение держит горутину и дескриптор, а из Механизма 1 мы знаем, что предела им нет.
Какой таймаут что ограничивает — главное, что здесь спрашивают:
ReadHeaderTimeout— только чтение заголовков. Документация советует именно его: большинство пользователей предпочтутReadHeaderTimeout, потому чтоReadTimeoutне даёт обработчику решать, что медленно для тела конкретного запроса;ReadTimeout— всё чтение запроса вместе с телом;WriteTimeout— отсчитывается от чтения заголовков, значит, покрывает и работу обработчика: долгий обработчик упрётся в него, даже если сам ответ пишется быстро;IdleTimeout— простой между запросами на соединении keep-alive.
Практичный набор для короткого API, который называют на собеседовании, —
ReadHeaderTimeout и IdleTimeout, плюс WriteTimeout. Это рекомендация, а
не правило: для потоковых ответов — SSE, длинных загрузок, долгих соединений
— серверный WriteTimeout оборвал бы законный ответ. Там срок записи задают
на запрос: http.NewResponseController(w), метод SetWriteDeadline, а для
всего соединения оставляют только защиту на чтение заголовков.
Рядом с таймаутами стоит ещё один встроенный предел, и он как раз не ноль:
MaxHeaderBytes.
5) MaxHeaderBytes ПО УМОЛЧАНИЮ
──────────────────────────────
http.DefaultMaxHeaderBytes = 1048576
заголовок 4096 байт: 200 OK
заголовок 2097152 байт: 431 Request Header Fields Too Large
Мегабайт заголовков на запрос — много; если сервис их не ждёт, предел опускают.
Про то, почему таймаут ограничивает ожидание, но не работу, и почему таймауты по цепочке сервисов не складываются, — урок о таймаутах и дедлайнах в линии SRE.
Механизм 3: клиент ушёл — контекст отменён, работа идёт
Что сервер делает, когда клиент перестал ждать:
For incoming server requests, the context is canceled when the client's
connection closes, the request is canceled (with HTTP/2), or when the ServeHTTP
method returns.
Для входящих запросов сервера контекст отменяется, когда закрывается соединение клиента, когда запрос отменён (в HTTP/2) или когда метод ServeHTTP вернул управление.
Контекст отменяется, когда сервер обнаружил разрыв или отмену, — это сигнал для кода, а не мгновенное уведомление о том, что клиента нет. И больше сервер ничего не делает. Два обработчика с одинаковой работой на 500 мс, клиент ждёт 100 мс и уходит:
3) КОНТЕКСТ ЗАПРОСА, КОГДА КЛИЕНТ УШЁЛ
──────────────────────────────────────
клиент ждёт ответа 100 мс и уходит; работа обработчика — 500 мс
слушает r.Context() через 101 мс: остановился: context canceled
не слушает контекст через 500 мс: доделал работу; контекст: context canceled
Второй обработчик потратил 400 мс на ответ, который никто не прочитает, —
хотя его контекст был отменён уже давно. Отмена в Go — кооперативная:
остановится только код, который ctx.Done() слушает. Поэтому r.Context()
передают дальше — в запрос к базе, в исходящий HTTP-запрос, в долгий цикл. Как
устроена отмена контекста, разобрано в уроке о context.
Откуда берётся этот контекст, тоже настраивается: Server.BaseContext задаёт
базовый контекст всех запросов сервера, Server.ConnContext — контекст
соединения. Через них в обработчики попадают, например, общий сигнал
остановки или данные о соединении.
Механизм 4: Shutdown, Close — и ListenAndServe, который не ждёт
Остановить сервер можно двумя методами. Shutdown закрывает слушателей,
закрывает простаивающие соединения и ждёт, пока активные станут
простаивающими. Close немедленно закрывает слушателей и обычные
соединения — новые, активные и простаивающие. Пять запросов по 300 мс, вызов
через 100 мс после их старта:
4) ОСТАНОВКА: Shutdown, Close И ВОЗВРАТ ИЗ Serve
────────────────────────────────────────────────
Shutdown через 100 мс после старта пяти запросов по 300 мс:
Serve вернул http: Server closed через 0 мс после вызова
Shutdown вернул управление через 276 мс
запросы в полёте: 200 — 5, ошибка — 0
новый запрос после вызова: отклонён
Close через 100 мс после старта пяти запросов по 300 мс:
Serve вернул http: Server closed через 0 мс после вызова
Close вернул управление через 1 мс
запросы в полёте: 200 — 0, ошибка — 5
новый запрос после вызова: отклонён
Shutdown дождался всех пяти, Close оборвал все пять. Но смотреть надо на
первую строку каждого блока: Serve вернулся через 0 мс — в обоих
случаях, когда запросы ещё работали. Документация предупреждает об этом
прямо:
When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS
immediately return ErrServerClosed. Make sure the program doesn't exit and waits
instead for Shutdown to return.
Когда вызван Shutdown, Serve, ListenAndServe и ListenAndServeTLS немедленно возвращают ErrServerClosed. Убедитесь, что программа не завершается, а ждёт возврата Shutdown.
Типичный код, который выглядит правильно и обрывает запросы:
go func() {
<-sigterm
srv.Shutdown(ctx)
}()
log.Fatal(srv.ListenAndServe()) // вернётся сразу — и main завершитсяListenAndServe вернул ErrServerClosed через 0 мс, log.Fatal завершил
процесс — и Shutdown не успел дождаться ни одного запроса. Правильный
порядок обратный: ListenAndServe — в горутине, а main ждёт сигнала, зовёт
Shutdown с контекстом-дедлайном и дожидается его возврата.
ErrServerClosed из ListenAndServe — не ошибка, а штатный сигнал.
Перехваченные соединения — вне жизненного цикла сервера. WebSocket и всё,
что обработчик забрал себе через Hijack, ни Close, ни Shutdown не трогают:
4) Close И ПЕРЕХВАЧЕННОЕ СОЕДИНЕНИЕ
───────────────────────────────────
после srv.Close() перехваченное соединение ответило: "эхо: после Close"
Для них есть RegisterOnShutdown: функция вызывается при Shutdown и
должна запустить остановку, специфичную для протокола, — например,
разослать клиентам WebSocket кадр закрытия, — но не ждать её завершения.
Дождаться самих соединений — отдельная работа приложения.
Что делать до Shutdown — снять экземпляр с балансировщика, дать ему
дренироваться, уложиться в льготный срок оркестратора, — не специфика Go, и
это разобрано в уроке о graceful shutdown линии SRE.
Глубже: паника в обработчике и шаблоны ServeMux
Паника не роняет процесс. В отличие от паники в обычной горутине, паника в обработчике перехватывается сервером:
If ServeHTTP panics, the server (the caller of ServeHTTP) assumes that the
effect of the panic was isolated to the active request.
Если ServeHTTP паникует, сервер (тот, кто вызвал ServeHTTP) считает, что последствия паники ограничены активным запросом.
5) ПАНИКА В ОБРАБОТЧИКЕ
───────────────────────
GET /boom: ошибка у клиента: true
GET /ok после паники: 200
журнал сервера: http: panic serving 127.0.0.1:…: boom
Клиент ответа не получил, сервер записал стек в журнал и продолжил работу.
Что именно стало с соединением, зависит от протокола — документация говорит:
сервер либо закрывает сетевое соединение, либо отправляет HTTP/2
RST_STREAM. Тот же обработчик под HTTP/1.1 и под HTTP/2:
3) ПАНИКА В ОБРАБОТЧИКЕ: HTTP/1.1 ПРОТИВ HTTP/2
───────────────────────────────────────────────
HTTP/1.1: /ok -> 200 HTTP/1.1; /boom -> ошибка; /ok -> 200 HTTP/1.1
соединение переиспользовано: false, соединений всего 3, обработчик /boom вызван 2 раз
HTTP/2 : /ok -> 200 HTTP/2.0; /boom -> ошибка; /ok -> 200 HTTP/2.0
соединение переиспользовано: true, соединений всего 1, обработчик /boom вызван 1 раз
В HTTP/1.1 соединение закрыто, и следующий запрос пошёл по новому. В HTTP/2
сброшен только поток запроса — соединение осталось жить, по нему пошёл
следующий запрос. И находка, которой не ждали: под HTTP/1.1 обработчик /boom
вызвался дважды — клиентский Transport сам повторил идемпотентный GET
после сетевой ошибки на уже использованном соединении. Для обработчика с
побочными эффектами на GET это значит два выполнения.
Защита — только для горутины обработчика: если обработчик запустил свою горутину и паника случилась в ней, процесс упадёт (механизм — в уроке о defer, panic и recover). И «последствия ограничены запросом» — предположение сервера, а не гарантия: мьютекс, захваченный до паники и не отпущенный, остаётся захваченным.
Шаблоны ServeMux с Go 1.22 умеют метод и параметр пути:
6) ServeMux С ШАБЛОНАМИ GO 1.22
───────────────────────────────
GET /items/42 -> 200 id=42
POST /items/42 -> 405 Method Not Allowed | Allow: GET, HEAD
GET /items/42/x -> 404 404 page not found
Шаблон GET /items/{id}, значение — через r.PathValue("id"); на чужой
метод — 405 с заголовком Allow, без единой строки кода. Если запросу
подходят несколько шаблонов, выбирается самый конкретный:
If two or more patterns match a request, then the most specific pattern takes
precedence.
Если запросу подходят два шаблона или больше, побеждает самый конкретный.
Отсюда ловушка, которую проверяет первая задача практики: 405 сервер отдаёт, только если путь не подходит больше ни под один шаблон.
Как отвечать на собеседовании
Короткий ответ: обработчик — интерфейс http.Handler, ServeMux
раскладывает запросы по путям, промежуточный слой — функция Handler → Handler. Сервер обслуживает каждое соединение в своей горутине (в HTTP/2 —
каждый поток), поэтому общее изменяемое состояние обработчиков нужно
защищать. В продакшене — свой
http.Server с таймаутами и остановка через Shutdown.
Этого достаточно, чтобы ответить верно. Дальше — то, что добавляют, если собеседник копает.
Если интервьюер копает глубже
На «какие таймауты» — что по умолчанию. «У http.Server{} все четыре
нулевые, то есть их нет. Клиент, не дославший заголовки, у меня держал
соединение и через 2 с; с ReadHeaderTimeout 300 мс — закрыт через 301 мс.
Минимум — ReadHeaderTimeout и IdleTimeout; WriteTimeout покрывает и
работу обработчика».
На «клиент ушёл» — кооперативная отмена. «r.Context() отменяется, но
работу останавливает только код, который его слушает: у меня обработчик без
проверки контекста доделал 500 мс после того, как клиент ушёл через 100.
Контекст запроса передают в базу и исходящие вызовы».
На graceful shutdown — ловушка с возвратом. «Shutdown дожидается
запросов, Close обрывает. Но ListenAndServe возвращает ErrServerClosed
сразу после вызова Shutdown — у меня через 0 мс. Если main на этом
завершается, запросы обрываются. Ждать надо возврата Shutdown».
На «а если обработчик запаникует» — граница. «Сервер перехватит панику,
дошедшую до ServeHTTP, и запишет в журнал; процесс жив. В HTTP/1.1 он
закроет соединение, в HTTP/2 сбросит только поток. Но паника в горутине,
запущенной из обработчика, этой защитой не покрыта и уронит процесс».
На «как сервер обслуживает конкурентные запросы» — «горутина на соединение, а в HTTP/2 — на поток; встроенного пула и предела на число запросов нет: десять запросов по 200 мс на десяти соединениях у меня заняли 209 мс. Соединение — не запрос: keep-alive пропускает по нему запросы по очереди, HTTP/2 — одновременно».
Дальше спросят
Как ограничить число одновременных запросов?
Сам net/http этого не делает. Обычный способ — промежуточный слой с
семафором: буферизованный канал на N мест, обработчик занимает место перед
вызовом next и освобождает после; если места нет — сразу 503, а не очередь.
Для ограничения числа соединений есть golang.org/x/net/netutil.LimitListener.
Важно, что отказ лучше очереди: запрос, который ждал в очереди дольше таймаута клиента, выполнится впустую — ровно как обработчик из Механизма 3.
Как протестировать обработчик?
net/http/httptest. httptest.NewRecorder() — это ResponseWriter, который
запоминает код, заголовки и тело; обработчик вызывают напрямую,
h.ServeHTTP(rec, req), без сети. httptest.NewServer поднимает настоящий
сервер на loopback, когда нужно проверить клиентский код или промежуточный
слой целиком.
Обработчик, принимающий зависимости через интерфейсы, тестируется так же просто, как функция, — это разобрано в уроке о тестировании.
Что даёт Shutdown контекст с дедлайном?
Предел ожидания. Если запросы в полёте не закончились до дедлайна, Shutdown
возвращает ошибку контекста, а соединения остаются открытыми — после этого
обычно зовут Close. Дедлайн выбирают меньше льготного срока, который даёт
оркестратор между SIGTERM и SIGKILL, — иначе процесс убьют посреди ожидания.
И ещё одно: перехваченные соединения вроде WebSocket Shutdown не закрывает
и не ждёт. RegisterOnShutdown даёт момент, чтобы начать их остановку, но
ждать их функция не должна — ожидание организуют отдельно.
Зачем нужен ReadHeaderTimeout, если есть таймаут у клиента?
Таймаут клиента защищает клиента, а не сервер. Медленный или злонамеренный клиент сам решает, сколько держать соединение: в прогоне он держал его 2 с только потому, что так решил сам. Сервер без своего таймаута ресурс отдать не может — и каждое такое соединение держит горутину и дескриптор.
Частые заблуждения
У http.Server есть разумные таймауты по умолчанию
Все четыре — ноль, а ноль значит «не ограничено». Клиент, не дославший заголовки, держал соединение и через 2 с; с ReadHeaderTimeout 300 мс сервер закрыл его через 301 мс.
Когда клиент уходит, сервер прекращает обработку запроса
Сервер только отменяет r.Context(). Обработчик, который контекст не слушает, доделал всю работу — 500 мс после того, как клиент ушёл через 100.
ListenAndServe возвращается, когда Shutdown закончил
Сразу после вызова Shutdown — в прогоне через 0 мс, пока пять запросов ещё работали. Программа, завершающаяся по этому возврату, обрывает их. Ждать надо возврата самого Shutdown.
Паника в обработчике роняет весь сервер
Панику, дошедшую до ServeHTTP, сервер перехватывает: следующий запрос — 200, в журнале — http: panic serving. В HTTP/1.1 соединение закрывается, в HTTP/2 сбрасывается только поток. Уронит процесс паника в горутине, которую обработчик запустил сам.
Одна горутина сервера — это один запрос
Горутина — на соединение, а соединение — не запрос: десять запросов подряд по HTTP/1.1 keep-alive прошли по одному соединению, а в HTTP/2 десять одновременных запросов шли одним соединением с десятью обработчиками сразу.
На запрос неподходящим методом ServeMux всегда отвечает 405
Только если путь не подходит под другой шаблон. С шаблонами GET /users/{id} и /users/ запрос DELETE /users/7 ушёл во второй и получил 200.
Практика
Две задачи. Сначала ответьте, потом сверьтесь с настоящим выводом: в обеих правильный ответ взят из прогона скрипта, а не назначен.
Практика · что напечатает
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "user ", r.PathValue("id"))
})
mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "fallback")
})Практика · оцените
Проверка знаний
Как net/http обслуживает конкурентные запросы?
Это не пересказ и не отдельный текст: всё ниже взято из самой статьи — её выжимка, заголовки разборов, колонка «на самом деле» и таблица версий. Поэтому разойтись со статьёй эти тезисы не могут.
Суть
- HTTP-сервер в Go — это
http.Handlerиhttp.Server. Обработчик — любой тип с методомServeHTTP(w, r);ServeMuxраскладывает запросы по обработчикам;ListenAndServeслушает порт. Сервер готов в пять строк, и ровно поэтому вопросы на собеседовании начинаются после этих пяти строк. - Сервер конкурентен сам: на каждое соединение — своя горутина, а в HTTP/2 — на каждый поток запроса внутри соединения. Десять запросов на десяти соединениях к обработчику, который спит 200 мс, заняли 209 мс, а не две секунды. Отсюда первое следствие: изменяемое состояние, общее для обработчиков, требует синхронизации. И второе: фиксированного предела на число одновременных запросов
net/httpне ставит — ни пула обработчиков, ни таймаутов по умолчанию. Уhttp.Server{}все четыре таймаута нулевые, а ноль значит «не ограничено»: клиент, приславший начало заголовков и замолчавший, через 2 с всё ещё держал соединение; сReadHeaderTimeout300 мс сервер закрыл его через 301 мс. - Дальше — то, что отличает понимание от пересказа. Когда клиент ушёл,
r.Context()отменяется, но работу останавливает только тот обработчик, который этот контекст слушает: второй доделал свои 500 мс.Shutdownдожидается запросов в полёте,Closeих обрывает — ноListenAndServeвозвращается сразу после вызоваShutdown, через 0 мс, пока запросы ещё идут. Программа, которая на этом и завершается, обрывает их так же, какClose.
На самом деле
- Все четыре — ноль, а ноль значит «не ограничено». Клиент, не дославший заголовки, держал соединение и через 2 с; с
ReadHeaderTimeout300 мс сервер закрыл его через 301 мс. - Сервер только отменяет
r.Context(). Обработчик, который контекст не слушает, доделал всю работу — 500 мс после того, как клиент ушёл через 100. - Сразу после вызова
Shutdown— в прогоне через 0 мс, пока пять запросов ещё работали. Программа, завершающаяся по этому возврату, обрывает их. Ждать надо возврата самогоShutdown. - Панику, дошедшую до
ServeHTTP, сервер перехватывает: следующий запрос — 200, в журнале —http: panic serving. В HTTP/1.1 соединение закрывается, в HTTP/2 сбрасывается только поток. Уронит процесс паника в горутине, которую обработчик запустил сам. - Горутина — на соединение, а соединение — не запрос: десять запросов подряд по HTTP/1.1 keep-alive прошли по одному соединению, а в HTTP/2 десять одновременных запросов шли одним соединением с десятью обработчиками сразу.
- Только если путь не подходит под другой шаблон. С шаблонами
GET /users/{id}и/users/запросDELETE /users/7ушёл во второй и получил 200.
Что разобрано
- Что здесь на самом деле спрашивают
- База: Handler, ServeMux и промежуточный слой
- Механизм 1: горутина на соединение — и никакого встроенного предела
- Механизм 2: четыре таймаута, и все нулевые
- Механизм 3: клиент ушёл — контекст отменён, работа идёт
- Механизм 4: Shutdown, Close — и ListenAndServe, который не ждёт
- Глубже: паника в обработчике и шаблоны ServeMux
- Как отвечать на собеседовании
- Дальше спросят
- Частые заблуждения
- Практика
- Проверка знаний
Источники и что читать дальше
4 ИСТОЧНИКА
- Пакет net/http — Server.Serve и HandlerОфициальная документация. Модель конкурентности: «Serve accepts incoming HTTP connections on the listener l, creating a new service goroutine for each» (Serve принимает входящие HTTP-соединения на слушателе l и создаёт для каждого отдельную обслуживающую горутину). И что будет при панике: «If ServeHTTP panics, the server (the caller of ServeHTTP) assumes that the effect of the panic was isolated to the active request» (Если ServeHTTP паникует, сервер (тот, кто вызвал ServeHTTP) считает, что последствия паники ограничены активным запросом) — и дальше «either closes the network connection or sends an HTTP/2 RST_STREAM, depending on the HTTP protocol» (либо закрывает сетевое соединение, либо отправляет HTTP/2 RST_STREAM — в зависимости от протокола).https://pkg.go.dev/net/http#Server.Serve
- Пакет net/http — таймауты http.ServerОфициальная документация. Что значит ноль: «A zero or negative value means there will be no timeout» (Нулевое или отрицательное значение означает, что таймаута не будет). И почему чаще берут ReadHeaderTimeout: «most users will prefer to use ReadHeaderTimeout» (большинство пользователей предпочтут ReadHeaderTimeout).https://pkg.go.dev/net/http#Server
- Пакет net/http — Server.ShutdownОфициальная документация. Главная ловушка остановки: «When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS immediately return ErrServerClosed. Make sure the program doesn't exit and waits instead for Shutdown to return» (Когда вызван Shutdown, Serve, ListenAndServe и ListenAndServeTLS немедленно возвращают ErrServerClosed. Убедитесь, что программа не завершается, а ждёт возврата Shutdown). Про Close и перехваченные соединения: «Close does not attempt to close (and does not even know about) any hijacked connections, such as WebSockets» (Close не пытается закрыть перехваченные соединения, например WebSocket, и даже не знает о них). И что должен делать обработчик RegisterOnShutdown: «This function should start protocol-specific graceful shutdown, but should not wait for shutdown to complete» (Эта функция должна запустить корректную остановку, специфичную для протокола, но не должна ждать её завершения).https://pkg.go.dev/net/http#Server.Shutdown
- Пакет net/http — Request.Context и ServeMuxОфициальная документация. Когда отменяется контекст запроса: «For incoming server requests, the context is canceled when the client's connection closes, the request is canceled (with HTTP/2), or when the ServeHTTP method returns» (Для входящих запросов сервера контекст отменяется, когда закрывается соединение клиента, когда запрос отменён (в HTTP/2) или когда метод ServeHTTP вернул управление). Как ServeMux выбирает шаблон: «If two or more patterns match a request, then the most specific pattern takes precedence» (Если запросу подходят два шаблона или больше, побеждает самый конкретный).https://pkg.go.dev/net/http#ServeMux