17. HTTP и сетевое взаимодействие
«Хорошо в стандартах то, что их так много на выбор».
— Эндрю Таненбаум
Эволюция сетевых протоколов: HTTP/1.1 → HTTP/2 → HTTP/3 (QUIC)
flowchart TD
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
H1["HTTP/1.1 (TCP)"]:::warning -->|"Head-of-Line Blocking на уровне HTTP"| H2["HTTP/2 (TCP + TLS 1.2/1.3)"]:::accent
H2 -->|"Head-of-Line Blocking на уровне TCP при потере пакета"| H3["HTTP/3 (QUIC поверх UDP + TLS 1.3)"]:::success
subgraph H1Features["Особенности HTTP/1.1"]
F1["Текстовый формат, Keep-Alive, 1 запрос на TCP сокет одновременно"]:::process
end
subgraph H2Features["Особенности HTTP/2"]
F2["Бинарный фрейминг, мультиплексирование потоков в 1 TCP сокет, HPACK"]:::process
end
subgraph H3Features["Особенности HTTP/3"]
F3["Независимые потоки в UDP (нет TCP HOL), 0-RTT рукопожатие, Connection ID миграция"]:::success
end
Архитектура HTTP-сервера в Go (net/http)
Go реализует модель «горутина на каждое TCP-соединение» поверх неблокирующего netpoller ядра ОС:
flowchart TD
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
Listen["net.Listen('tcp', ':8080') -> сокет листенера"]:::entry --> AcceptLoop["Бесконечный цикл: l.Accept()"]:::process
AcceptLoop --> Fork["go c.serve(connCtx) (Новая горутина на каждый TCP-сокет!)"]:::accent
Fork --> ReadReq["Чтение заголовков и URL (bufio.Reader)"]:::process
ReadReq --> Match["Роутер http.ServeMux (Go 1.22+ method + path)"]:::process
Match --> Middleware["Middleware цепочка (Logging, Auth, Recovery)"]:::accent
Middleware --> Handler["Handler.ServeHTTP(w, r)"]:::success
Сравнение протоколов: gRPC vs REST
flowchart LR
classDef entry fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
classDef process fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
classDef accent fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
classDef success fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
classDef warning fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
subgraph REST["REST over HTTP/1.1"]
RJ["Текстовый JSON (тяжелый парсинг)"]:::warning
RC["Новый запрос = ожидание или отдельное TCP-соединение"]:::warning
RU["Слабая типизация (OpenAPI спецификации вручную)"]:::process
end
subgraph GRPC["gRPC over HTTP/2"]
GP["Бинарный Protobuf (быстрая кодогенерация, сжатие)"]:::success
GM["Мультиплексирование сотен RPC в одном сокете"]:::success
GS["Двунаправленный стриминг (Client/Server/Bidi)"]:::accent
GT["Строгий контракт .proto с генерацией Go-интерфейсов"]:::success
end
Навигатор по вопросам главы
- Как работает HTTP?
- Что такое TCP?
- Что такое handshake (рукопожатие)?
- Что такое keep-alive?
- Как работает
net/http? - Что такое middleware?
- Как работает пул соединений (connection pool)?
- Как работает
http.Transport? - Что такое
IdleConnTimeout? - Что такое TLS и как он работает в Go?
- Как сделать HTTP/2 сервер?
- Как работает
x/net/http2? - Как работает
http.Requestиhttp.ResponseWriter? - Что такое
http.Handler? - Как реализовать
ServeHTTP? - Как обрабатывать таймауты (
ReadTimeout,WriteTimeout,IdleTimeout)? - Как работает
contextв HTTP запросах? - Как отменить HTTP запрос?
- Как работает
net.Dial? - Что такое
net.Conn? - Как работает
net.Listen? - Как написать TCP сервер без
net/http? - Как обрабатывать WebSocket (
gorilla/websocket)? - Как работает
gRPC? - В чем преимущества
gRPCперед REST? - Как работает Protobuf и сериализация?
- Как загружать большие файлы через HTTP?
- Как работает докачка файлов (range request)?
- Как реализовать потоковую передачу (streaming) через HTTP?
- Как работает
http.ServeMuxи роутинг в Go 1.22+? - Как обрабатывать CORS?
- Как работает
http.Clientс прокси? - Что такое
httptraceи как его использовать для отладки? - Как работает
graceful shutdownдля HTTP сервера? - Как работает
SO_REUSEPORTв Go? - Как работает
fasthttpи чем он отличается отnet/http? - Как обрабатывать
multipart/form-data? - Как работает
Server-Sent Events (SSE)? - Как настроить keep-alive для клиента и сервера?
- Как работает
net.LookupHostдля разрешения DNS? - Как работает
net/http/httputil.ReverseProxy? - Что такое
DrainBodyи почему важно вычитывать тело ответа до конца? - Как работает
http.RoundTripperи как его можно мокать? - Как работать с
x-www-form-urlencodedданными? - Как работает
h2c(HTTP/2 без TLS)? - Как реализовать "long polling" с помощью Go?
- Как тестировать HTTP хендлеры с помощью
httptest? - Как ограничить количество входящих соединений на HTTP сервере?
Как работает HTTP?
Краткий ответ (30–60 секунд)
HTTP — текстовый протокол прикладного уровня, работающий по модели запрос-ответ поверх TCP (или TLS). Клиент отправляет запрос, состоящий из строки запроса, заголовков и опционального тела; сервер возвращает статус-код, заголовки и тело. Протокол не хранит состояние, но расширяется механизмами вроде кук, keep-alive и кэширования.
Развёрнутый ответ
HTTP (Hypertext Transfer Protocol) определяет формат сообщений и порядок обмена ими. Запрос состоит из:
- Метод (GET, POST, PUT, DELETE…), path, версия протокола.
- Заголовки (Host, Content-Type, Accept, Authorization и др.).
- Пустая строка (CRLF).
- Тело (опционально).
Ответ:
- Версия, статус-код (200, 404…), пояснение.
- Заголовки (Content-Length, Content-Type, Set-Cookie…).
- Тело.
Важные версии:
- HTTP/1.0: по умолчанию одно соединение — один запрос.
- HTTP/1.1: keep-alive по умолчанию, chunked transfer encoding, пайплайнинг (почти не используется).
- HTTP/2: бинарный фрейминг, мультиплексирование, server push, сжатие заголовков HPACK.
- HTTP/3: поверх QUIC на UDP, решает head-of-line blocking на транспортном уровне.
Пример сырого HTTP-запроса (HTTP/1.1):
GET /api/users HTTP/1.1
Host: example.com
Accept: application/json
Ответ:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 42
{"id":1,"name":"Alice"}
В Go HTTP реализован пакетом net/http, который скрывает детали парсинга и управления TCP, предоставляя интерфейсы Handler, RoundTripper, Client и Server.
Под капотом (internals)
В net/http сетевое взаимодействие абстрагировано через net.Conn, полученный из net.Dial (клиент) или net.Listen (сервер). Реализация HTTP/1.1 расположена в transport.go (http.Transport) и server.go (http.Server).
- Для чтения запросов используется
bufio.Reader, чтобы искать\r\n\r\n, затем парсинг заголовков вtextproto.Reader. - Тела читаются обёртками
http.body, которые отслеживают закрытие и обеспечивают ограничение размера (MaxBytesReader). http.Transportуправляет пулом TCP-соединений и кэширует их для keep-alive; каждое соединение обрабатывается в отдельной горутине.- Поддержка HTTP/2 реализована в
golang.org/x/net/http2, включая собственный фрейминг и мультиплексирование черезhttp2.Transportиhttp2.Server.
Типичные ошибки кандидатов
-
Не вычитывают тело ответа до конца при keep-alive
Даже если ответ не нужен, прочитанное до конца тело позволяет переиспользовать TCP-соединение. Не вычитанное тело приводит к утечке соединений и нехватке портов. Решение:io.Copy(io.Discard, resp.Body)передresp.Body.Close(). -
Забывают закрывать
resp.Bodyу клиента
Не закрытое тело удерживает соединение открытым и не возвращает его в пул. Это вызывает истощение файловых дескрипторов. Исправление: всегдаdefer resp.Body.Close(). -
Неправильное иcпользование Content-Length с chunked
Если тело не известно заранее и не установленContent-Length, Go автоматически использует chunked Transfer-Encoding. Некоторые серверы требуют явно задатьContent-Length: 0для пустого тела. Ошибка: не учитывать это при интеграции. -
Игнорирование порядка записи заголовков
Заголовки должны быть записаны до тела: в GoWriteHeaderвызывает отправку статус-кода и заголовков; любое изменение послеWriteигнорируется. Кандидаты часто пытаются установить заголовки после вызоваWrite.
Возможные дополнительные вопросы интервьюера
-
Как клиент определяет, что ответ окончен?
По значениюContent-LengthилиTransfer-Encoding: chunked(последовательность чанков, завершающихся нулевым чанком). При отсутствии обоих в HTTP/1.1 соединение может быть закрыто сервером. -
Чем отличается HTTP/1.1 от HTTP/2 в плане мультиплексирования?
HTTP/1.1 открывает несколько TCP-соединений для параллельных запросов, что приводит к конкуренции; HTTP/2 мультиплексирует множество потоков в одном TCP-соединении, избавляясь от head-of-line blocking на уровне приложения, но остаётся проблема на уровне TCP (решается HTTP/3). -
Как работает Upgrade-заголовок для WebSocket?
Клиент отправляетUpgrade: websocketи соответствующие ключи; сервер отвечает 101 Switching Protocols, и дальнейший обмен идёт в бинарном кадре поверх того же TCP-соединения.
Что такое TCP?
Краткий ответ (30–60 секунд)
TCP (Transmission Control Protocol) — протокол транспортного уровня, обеспечивающий надёжную, упорядоченную, с контролем потока и ошибок доставку байтового потока между приложениями. Работает поверх IP, использует подтверждения (ACK), повторную отправку потерянных пакетов и управление перегрузкой.
Развёрнутый ответ
TCP устанавливает двусторонний канал связи (сокет), гарантируя, что данные дойдут в правильном порядке и без дублирования. Основные механизмы:
- Тройное рукопожатие (SYN, SYN-ACK, ACK) для установления соединения.
- Последовательные номера — каждый байт нумеруется, приёмник отправляет ACK с номером следующего ожидаемого байта.
- Контрольные суммы — для выявления повреждений.
- Таймеры и ретрансмиссии — если ACK не получен, сегмент пересылается.
- Окно приёма — управление потоком: получатель сообщает, сколько данных он готов принять.
- Управление перегрузкой — алгоритмы (Slow Start, Congestion Avoidance, Fast Retransmit/Recovery) для предотвращения перегрузки сети.
- Четырёхстороннее закрытие (FIN, ACK) для корректного разрыва.
TCP ориентирован на соединение, что делает его идеальным для HTTP, TLS, SSH и т.д. В Go TCP представлен интерфейсом net.Conn из пакета net.
Под капотом (internals)
В Linux TCP реализован в ядре. Go использует системные вызовы через net.Dial("tcp", ...), который вызывает socket(2), connect(2). Чтение/запись — через read(2)/write(2) на файловом дескрипторе. Во время чтения горутина блокируется, но сетевой поллер (epoll/kqueue) позволяет эффективно ожидать событий на многих дескрипторах без блокировки потока ОС. Структура net.Conn в Go обёрнута в *net.TCPConn, которая позволяет настраивать параметры (KeepAlive, NoDelay).
Типичные ошибки кандидатов
-
Не настройка TCP Keep-Alive для долгоживущих соединений
Если одна сторона молчит, промежуточные устройства (NAT, фаерволы) могут сбросить состояние. Без keep-alive соединение висит до таймаута уровня приложения. Решение:tcpConn.SetKeepAlive(true)и установка периода. -
Предположение, что
Writeсразу отправляет данные
Writeпомещает данные в буфер отправки ядра, а алгоритм Nagle может откладывать отправку маленьких пакетов. Для низкой задержки можно отключить Nagle черезSetNoDelay(true). -
Неверная обработка коротких чтений
TCP — потоковый протокол,Readможет вернуть меньше запрошенного. Нельзя рассчитывать, что одинReadвернёт ровно одно сообщение. Нужно буферизовать и парсить протокол уровня приложения. -
Попытка полагаться на RST как на штатный разрыв
Close()обычно отправляет FIN, ожидает ACK и переходит в состояние TIME_WAIT. При аварийном завершении можно вызватьSetLinger(0)для отправки RST, но это не рекомендуется для обычного потока.
Возможные дополнительные вопросы интервьюера
-
Что такое TIME_WAIT и почему он важен?
Состояние после активного закрытия, длится 2MSL (обычно 60с), гарантирует, что задержавшиеся сегменты не помешают новому соединению с теми же портами. На высоконагруженных клиентах его переполнение вызывает нехватку локальных портов, решаетсяSO_REUSEADDRили пулом соединений. -
Чем TCP отличается от UDP?
UDP — без соединения, не гарантирует доставку и порядок, нет контроля перегрузки. Используется в DNS, стриминге, HTTP/3 (через QUIC). -
Как работает управление перегрузкой в TCP?
Основные алгоритмы: Tahoe/Reno/NewReno, CUBIC (по умолчанию в Linux), BBR. Они управляют размером окна перегрузки (cwnd), чтобы избежать переполнения сети.
Что такое handshake (рукопожатие)?
Краткий ответ (30–60 секунд)
Handshake — начальная фаза установления защищённого или надёжного канала. В TCP это тройное рукопожатие (SYN, SYN-ACK, ACK) для синхронизации последовательных номеров. В TLS — более сложный обмен для согласования версии, шифров и аутентификации.
Развёрнутый ответ
Для TCP трёхстороннее рукопожатие:
- Клиент отправляет SYN со случайным номером последовательности (
seq=x). - Сервер отвечает SYN-ACK: подтверждает
x+1и предлагает свойseq=y. - Клиент отвечает ACK с
seq=x+1, ack=y+1.
После этого соединение установлено. Этот процесс исключает старые дубликаты и синхронизирует номера.
TLS рукопожатие (минимальный обзор):
- ClientHello: версия TLS, наборы шифров, случайные данные, расширения (SNI).
- ServerHello: выбранный набор, сертификат, случайные данные, запрос сертификата клиента (опционально).
- Аутентификация и обмен ключами (RSA, DHE, ECDHE).
- Обе стороны вычисляют сессионные ключи.
- Завершающие сообщения Finished для подтверждения.
В TLS 1.3 рукопожатие сокращено до 1-RTT, иногда 0-RTT с предварительным ключом.
Под капотом (internals)
В Go TCP-рукопожатие происходит внутри net.Dial("tcp", addr), который блокирует горутину до завершения соединения. При использовании http.Transport с TLS, TLS handshake запускается после установления TCP-соединения, внутри tls.Dial или встроенного клиента. Код в crypto/tls: Client и Server структуры, методы Hand shake(). Используется асинхронная машина состояний (finite state machine). Сертификаты загружаются в tls.Config.
Типичные ошибки кандидатов
-
Путаница между TCP и TLS handshake
Кандидаты часто говорят о TLS, когда спрашивают о TCP. Важно чётко разделять: TCP — транспортная синхронизация, TLS — криптографическая. При отладке таймаутов сначала смотрят TCP handshake черезnetstatили eBPF, затем TLS. -
Игнорирование влияния round-trip time (RTT)
Каждое сообщение handshake добавляет RTT. TLS 1.2 требует 2-RTT, TLS 1.3 — 1-RTT. Приложение должно учитывать это для latency-sensitive сервисов; используйте 0-RTT только для идемпотентных операций, чтобы избежать replay-атак. -
Не обновление корневых сертификатов
В Go системный корневой пул подгружается из ОС; в контейнерах иногда отсутствуют. Ошибка: "x509: certificate signed by unknown authority". Решение: встроить нужный сертификат вtls.Config.RootCAs. -
Задание
InsecureSkipVerify: trueв production
Отключает проверку цепочки сертификатов, подвергая MITM-атакам. Никогда не использовать в проде, лучше добавить CA в доверенные.
Возможные дополнительные вопросы интервьюера
-
Как работает TCP Fast Open (TFO)?
Позволяет отправлять данные в SYN-пакете на основе криптографического cookie, сохранённого с предыдущего соединения. Сокращает задержку. В Go поддерживается через настройку сокета, но не через стандартныйnetнапрямую без дополнительных syscall. -
Что такое Perfect Forward Secrecy в TLS?
Свойство, при котором компрометация долговременного ключа сервера не раскрывает прошлые сессии. Достигается через эфемерные алгоритмы (ECDHE). В Go это включено по умолчанию для TLS 1.2+. -
Как в Go установить минимальную версию TLS?
tls.Config{MinVersion: tls.VersionTLS12}. Рекомендуется задавать явно, чтобы избежать старых уязвимых версий.
Что такое keep-alive?
Краткий ответ (30–60 секунд)
Keep-alive — механизм повторного использования TCP-соединения для нескольких HTTP-запросов вместо открытия нового на каждый запрос. В HTTP/1.1 включён по умолчанию, управляется заголовком Connection: keep-alive. Снижает накладные расходы на TCP рукопожатие и медленный старт.
Развёрнутый ответ
В HTTP/1.0 после ответа сервер закрывал соединение. HTTP/1.1 ввёл постоянные соединения: сервер не закрывает соединение после ответа, а ждёт следующего запроса. Если клиент или сервер хочет разорвать, отправляется Connection: close. Для обнаружения разрыва на уровне TCP используют keep-alive пакеты (TCP keep-alive), но чаще говорят именно о HTTP keep-alive — переиспользовании соединения.
Преимущества:
- Меньше CPU и памяти на установку соединений.
- Уменьшение задержки за счёт исключения фазы рукопожатия и slow-start TCP.
- Лучшая утилизация пропускной способности.
Недостатки: может возникнуть "head-of-line blocking" в HTTP/1.1 из-за последовательной обработки запросов; сервер должен ограничивать максимальное время простоя соединения и количество запросов на соединение.
Под капотом (internals)
В net/http.Transport пул простаивающих соединений организован по ключу (схема, адрес). Когда Client.Do() завершён и тело ответа прочитано до конца, transport пытается вернуть *persistConn в idleConn map (или список). При следующем запросе к тому же хосту Transport берёт готовое соединение из пула, экономя на Dial. За простаивание отвечает IdleConnTimeout. На уровне TCP SetKeepAlive отправляет keep-alive зонды с интервалом, что помогает выявлять мёртвые соединения. В http.Server idle-таймаут (IdleTimeout) определяет, как долго держать простаивающее соединение открытым от клиента.
Типичные ошибки кандидатов
-
Неверное понимание области действия keep-alive
Путают HTTP keep-alive (повторное использование TCP для нескольких HTTP запросов) и TCP keep-alive (зонды ОС для обнаружения обрыва). На интервью нужно разграничивать, но знать, что они взаимодополняют: HTTP keep-alive может привести к полуоткрытым соединениям, которые TCP keep-alive обнаруживает. -
Не закрывают тело ответа → утечка keep-alive соединений
http.Transportне может вернуть соединение в пул, пока тело не дочитано и не закрыто. Это приводит к постоянному росту числа соединений. Исправление:io.Copy(io.Discard, resp.Body)+resp.Body.Close(). -
Установка слишком больших
IdleConnTimeout
Длительное простаивание может занять ресурсы сервера/клиента и привести к закрытию со стороны промежуточного прокси или фаервола, что вызовет ошибки "connection reset". Нужно настраивать таймауты со знанием инфраструктуры (обычно 60-90 сек). -
Не использование keep-alive для внутренних сервисов
По умолчаниюhttp.Serverдержит keep-alive, но если клиент не переиспользует соединения (каждый раз новыйhttp.Client), то преимущества теряются. Следует использовать глобальныйhttp.Clientс настроеннымTransport.
Возможные дополнительные вопросы интервьюера
-
Как определить оптимальное значение
MaxIdleConnsPerHost?
Оно должно быть не меньше ожидаемого параллельного числа запросов к одному хосту, но не больше лимита файловых дескрипторов. По умолчанию 2 — это слишком мало; для бэкендов ставят 100 и больше. -
Как сервер может форсировать закрытие keep-alive соединения?
Установкой заголовкаConnection: closeв ответе. В Go:w.Header().Set("Connection", "close"). Такжеhttp.Serverзакроет соединение, если исчерпанMaxHeaderBytesили при фатальной ошибке. -
Влияет ли HTTP pipelining на keep-alive?
Это расширение HTTP/1.1, позволяющее отправлять несколько запросов без ожидания ответов, но из-за сложностей реализации почти не применяется; в Go по умолчанию отключён. Без пайплайнинга keep-alive всё ещё эффективен.
Как работает net/http?
Краткий ответ (30–60 секунд)
Пакет net/http предоставляет клиент и сервер HTTP. На стороне сервера: создание http.Server, регистрация обработчиков через http.Handle или ServeMux, вызов ListenAndServe(). Клиент: создание http.Client, выполняющего запросы через http.Transport, который управляет TCP-соединениями, TLS, прокси и редиректами.
Развёрнутый ответ
Основные компоненты сервера:
http.Server— структура с адресом, обработчиком (обычноhttp.DefaultServeMuxили свойhttp.Handler), таймаутами (ReadTimeout,WriteTimeout,IdleTimeout),TLSConfig.ServeMux(мультиплексор) сопоставляет URL-путь с обработчиком. В Go 1.22+ поддерживает методы и именованные сегменты ({name}).http.Handler— интерфейс с методомServeHTTP(ResponseWriter, *Request).- Цепочка обработки:
Server.connв отдельной горутине читает запрос с помощьюreadRequest(), создаётresponseи вызывает обработчик. После возврата горутины возможно повторное использование соединения.
Клиент:
http.Client— точка входа, содержитTransport(по умолчаниюhttp.DefaultTransport) для выполнения круглых проходов (round trips),CheckRedirect,Jar(куки).http.Requestсоздаётсяhttp.NewRequest(илиhttp.NewRequestWithContext), затемclient.Do(req).DefaultTransport— экземплярhttp.Transportс разумными настройками таймаутов, пула соединений и поддержкой HTTP/2.
Под капотом (internals)
Ключевые файлы в net/http:
server.go:Serve,ListenAndServe, accept loop. Для каждого входящего соединения запускаетсяgo c.serve(ctx).transport.go:RoundTripлогика, пул свободных соединений (idleConn),dialConnдля создания нового TCP/TLS.response.go:responseструктура, реализующаяResponseWriter.request.go:Request, парсинг формы, cookie.h2_bundle.go: содержит встроенный HTTP/2 сервер (в Go 1.6+).
Сервер использует net.Listen для получения net.Listener. В цикле Accept() возвращает net.Conn. Для каждого соединения создаются conn структуры с буферами (bufio.Reader, bufio.Writer). HTTP/1.1 поддерживает keep-alive: после обработки запроса соединение не закрывается, а ожидает следующий запрос, отслеживая таймауты.
Типичные ошибки кандидатов
-
Использование DefaultServeMux в production без осторожности
Любая библиотека может зарегистрировать обработчики наhttp.Handle, что приводит к конфликтам или уязвимостям (например,net/http/pprof). Решение: всегда создавайте явныйhttp.NewServeMux(). -
Забывают про контекст запроса
Обработчики, которые запускают длительные операции, должны проверятьreq.Context().Done(), иначе горутина не завершится при обрыве соединения, утекая. Исправление: пробрасывать контекст в БД, gRPC, etc. -
Неверная запись статус-кода и заголовков
ВызовWriteHeader()посылает статус и все записанные заголовки; любое изменение заголовков после игнорируется. Часто пишут заголовки послеWrite(), что приводит к нарушению протокола. -
Не использование
Server.MaxHeaderBytes
По умолчанию 1MB, но злоумышленник может отправить запрос с огромными заголовками и забить память. Стоит установить разумный лимит.
Возможные дополнительные вопросы интервьюера
-
Чем отличается
HandleотHandleFunc?
Handleпринимаетhttp.Handler,HandleFunc— функцию с сигнатуройfunc(ResponseWriter, *Request), которая автоматически оборачивается вhttp.HandlerFunc. -
Как
net/httpобрабатывает concurrent requests?
Каждое входящее соединение обслуживается в отдельной горутине. Это масштабируется благодаря шедулеру Go и эффективному сетевому поллеру (epoll/kqueue). Однако при большом количестве соединений может потребоваться лимитирование через семафоры. -
Можно ли динамически добавлять обработчики в ServeMux после запуска сервера?
Формально можно, ноServeMuxне потокобезопасен для записи. В Go 1.22+ естьmux.Handler()для получения текущего обработчика, но изменение после старта не рекомендуется.
Что такое middleware?
Краткий ответ (30–60 секунд)
Middleware — функция, оборачивающая http.Handler и добавляющая сквозную функциональность: логирование, аутентификацию, трассировку, сжатие, CORS. Обычно имеет сигнатуру func(http.Handler) http.Handler. Может объединяться в цепочки.
Развёрнутый ответ
В Go middleware встраивается как прослойка между мультиплексором и конечным обработчиком. Пример типичного middleware:
func LoggingMiddleware(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))
})
}
Применение: mux := http.NewServeMux(); mux.HandleFunc("/", handler); loggedMux := LoggingMiddleware(mux); http.ListenAndServe(":8080", loggedMux).
Middleware может модифицировать запрос (например, добавлять request ID в контекст) или ответ (обёртывать ResponseWriter для захвата статус-кода, как это делает http.ResponseController в Go 1.20+). Можно создавать цепочки с помощью сторонних библиотек (gorilla/mux, chi) или вручную: h = middleware1(middleware2(h)).
Под капотом (internals)
Поскольку в Go http.Handler — интерфейс, обёртывание просто создаёт новый Handler, вызывающий оригинальный. Никакой магии: цепочка вызовов ServeHTTP разворачивается синхронно. Для обёртывания ResponseWriter ранние версии требовали реализации http.Hijacker, http.Flusher. Начиная с Go 1.20, http.ResponseController позволяет писать универсальные обёртки без знания конкретного типа доступа. Middleware может использовать w.(http.Flusher).Flush() для стриминга.
Типичные ошибки кандидатов
-
Изменение ответа после вызова
next.ServeHTTP
Если middleware пытается установить заголовки или статус-код после того, как обработчик уже вызвалWriteHeader, это игнорируется или вызывает panic. Правильно: модифицировать запрос до вызоваnext, а ответ перехватывать через обёрткуResponseWriter. -
Забывают вызвать
next.ServeHTTP
Тогда запрос не достигает конечного обработчика, клиент получает 200 OK с пустым телом (или таймаут). Явная ошибка, часто при условных блокировках авторизации: нужно либо вызватьnext, либо написать ошибку напрямую. -
Паника при приведении
ResponseWriterк интерфейсу без проверки
Например,w.(http.Hijacker)— не всеResponseWriterподдерживают хайджекинг, особенно при использовании HTTP/2. Нужно проверять приведение типов:if hijacker, ok := w.(http.Hijacker); ok { ... }. -
Middleware, который не гарантирует освобождение ресурсов при панике
Если middleware аллоцирует ресурс (контекст, транзакцию), но не используетdefer, паника вnextможет оставить ресурс неосвобождённым. Используйтеdeferвнутри своего обработчика.
Возможные дополнительные вопросы интервьюера
-
Как организовать приоритет middleware?
"Луковичная" модель: внешний обработчик выполняется первым до вызоваnextи последним после. Например:Recovery -> Logger -> Auth -> Handler. Порядок важен: Recovery должен быть снаружи, чтобы ловить паники во внутренних слоях. -
Как передать данные через middleware в обработчик?
Использоватьcontext.WithValueиr = r.WithContext(ctx). Не злоупотреблять: ключи должны быть определёнными типами, а не строками, для избежания коллизий. -
Что такое
http.ResponseControllerи как он помогает в middleware?
Позволяет middleware устанавливать таймауты чтения, перезаписывать статус и т.д. без прямого знания конкретного типаResponseWriter. Появился в Go 1.20:rc := http.NewResponseController(w); можноrc.SetReadDeadline(...)до вызова хендлера.
Как работает пул соединений (connection pool)?
Краткий ответ (30–60 секунд)
Пул соединений — кэш установленных TCP-соединений, используемый http.Transport для повторного применения в последующих запросах к тому же хосту. Это уменьшает задержки на рукопожатия и экономит ресурсы. Управляется параметрами MaxIdleConns, MaxIdleConnsPerHost, IdleConnTimeout.
Развёрнутый ответ
http.Transport содержит карту idleConn (тип map[connectMethodKey][]*persistConn), где ключ включает схему (http/https), адрес, прокси-адрес. После завершения запроса, если ответ прочитан до конца и соединение не закрыто сервером, persistConn помещается в пул простаивающих. При следующем RoundTrip транспорт ищет подходящее idle-соединение. Если нет — создаёт новое (ограничено MaxConnsPerHost). Пул также обрабатывает закрытие "мёртвых" соединений и очистку по таймауту.
Параметры:
MaxIdleConns— общее число простаивающих соединений во всём транспорте (по умолчанию 100).MaxIdleConnsPerHost— на конкретный хост (по умолчанию 2).IdleConnTimeout— сколько держать незанятое соединение (по умолчанию 90 сек).MaxConnsPerHost— максимальное общее количество соединений с хостом.
Корректное поведение клиента требует закрывать тело ответа (даже если не нужно) и читать его до конца, иначе соединение не возвращается в пул.
Под капотом (internals)
Внутренняя структура persistConn содержит net.Conn, обёрнутый в буферы, и управляет жизненным циклом: чтение ответа, закрытие. В методе tryPutIdleConn после обработки ответа пытается вернуть соединение в t.idleConn. Если пул переполнен (по лимитам), соединение закрывается. Также используются wantConn-запросы для ожидания свободного соединения, когда лимит новых соединений исчерпан. По CloseIdleConnections() все простаивающие закрываются (например, перед сменой DNS).
Типичные ошибки кандидатов
-
Ошибка с
MaxIdleConnsPerHost= 0 (отключение пула)
Если установлен в 0, idle соединения не кешируются, каждое новое соединение создаётся заново, что убийственно для производительности. Надо устанавливать >0, по умолчанию 2 — маловато для микросервисов. -
Не читают тело ответа для keep-alive
Соединение не возвращается в пул, если остались непрочитанные данные. В итоге транспорт продолжает открывать новые соединения, превышая лимиты и упираясь в порты. Побочный эффект — утечка файловых дескрипторов. -
Игнорирование
Response.Header.Get("Connection")
Если сервер присылаетConnection: close, такое соединение не переиспользуется. Кандидаты иногда удивляются, почему соединения закрываются. Нужно логировать и анализировать. -
Неправильная работа с прокси и пулом
Для HTTPS через прокси метод CONNECT создаёт туннель; транспорт кэширует эти туннели. Неучёт прокси-аутентификации иProxyConnectHeaderможет привести к тому, что все соединения используют первоначальные учётные данные.
Возможные дополнительные вопросы интервьюера
-
Как отключить keep-alive на конкретный запрос?
Установить в запросеreq.Close = trueилиreq.Header.Set("Connection", "close"). Или на уровне транспортаDisableKeepAlives: true. -
Как диагностировать утечку соединений?
Использоватьnet/http/pprofили метрики: посмотреть горутины вnet/http.(...persistConn).readLoop, метрикиhttp.Clientчерезexpvarили трассировкуhttptrace. -
Почему
MaxIdleConnsPerHostпо умолчанию 2, а не больше?
Историческое значение, чтобы избежать слишком большого числа открытых idle-соединений к одному хосту при использованииDefaultTransportмногими горутинами. В продакшене нужно увеличивать.
Как работает http.Transport?
Краткий ответ (30–60 секунд)
http.Transport — реализация http.RoundTripper, управляющая созданием, кэшированием и пуллингом TCP/TLS соединений для выполнения HTTP-запросов. Обрабатывает прокси, редиректы (опционально), keep-alive, таймауты, TLS. По умолчанию используется http.DefaultTransport, который включает HTTP/2.
Развёрнутый ответ
Transport содержит всю логику отправки запросов по сети. При RoundTrip:
- Определяется адрес и подключение (возможно, через прокси).
- Из
idleConnизвлекается свободноеpersistConn, если есть, или создаётся новое (dialConn). - Запрос записывается в соединение, читается ответ.
- После закрытия тела ответа соединение возвращается в пул (если не закрыто).
Важные поля:
DialContext— функция для создания сетевого соединения.TLSClientConfig— настройки TLS.MaxIdleConns,MaxIdleConnsPerHost,IdleConnTimeout— пул.MaxConnsPerHost— ограничение на общее число соединений (не только простаивающих).ResponseHeaderTimeout— таймаут ожидания заголовков ответа.ExpectContinueTimeout— для 100-continue.Proxy— функция, возвращающая URL прокси.DialTLSContext— собственный TLS-дайлер для HTTP/2 или тонкой настройки.
По умолчанию Transport поддерживает и HTTP/1.1, и HTTP/2 (через x/net/http2). При первом соединении с сервером и наличии поддержки, транспорт обновляет протокол до HTTP/2 (ALPN при TLS или через http2.ConfigureTransport).
Под капотом (internals)
http.Transport — сложная структура с множеством приватных полей (idleMu, idleConn, reqMu, reqCanceler, altProto). При вызове roundTrip создаётся wantConn — запрос на получение persistConn. Для ожидания соединения используется канал; может запускаться горутина dialConnFor. Каждое persistConn имеет две горутины — readLoop и writeLoop, общающиеся через каналы, реализуя симметричную модель обработки. Это позволяет корректно обрабатывать закрытие и отмену. HTTP/2 реализован через altProto с транспортом http2.Transport.
Типичные ошибки кандидатов
-
Изменение
DefaultTransport
http.DefaultTransport— глобальный объект, его изменение влияет на всех клиентов. Лучше создавать кастомный транспорт и клонировать настройки. -
Не настройка
MaxConnsPerHostпри большом трафике
По умолчанию 0 (без ограничения). При атаке или всплеске может быть создано слишком много соединений, исчерпывающих лимит ОС по файловым дескрипторам. Рекомендуется устанавливать разумное ограничение. -
Отсутствие
ResponseHeaderTimeout
По умолчанию 0 — бесконечное ожидание. Сервер может "залипать". Установите таймаут, иначе рискуете зависшими горутинами. -
Несинхронизированная смена
DialContextв работающем клиенте
После измененияDialContextна лету новые соединения будут использовать его, но старые останутся в пуле со старыми параметрами; можно получить сюрпризы с привязкой к сетевому интерфейсу или таймаутами.
Возможные дополнительные вопросы интервьюера
-
Как отключить HTTP/2 в
Transport?
Не инициализироватьTLSNextProtoи не вызыватьhttp2.ConfigureTransport. Можно принудительно задатьTLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)— пустая карта отключит HTTP/2. -
Чем отличается
Transport.DialContextотTransport.DialTLSContext?
DialContextсоздаёт сырое TCP-соединение, а TLS наслаивается позже.DialTLSContext— создаёт сразу TLS-соединение (используется для HTTP/2, когда нужен полный контроль над TLS). По умолчаниюDialTLSContextвызываетDialContextи делает TLS handshake. -
Как очистить пул соединений?
transport.CloseIdleConnections()закрывает все простаивающие соединения. Полезно при изменении DNS или перед остановкой процесса.
Что такое IdleConnTimeout?
Краткий ответ (30–60 секунд)
IdleConnTimeout — поле http.Transport, определяющее максимальное время, в течение которого простаивающее keep-alive соединение может находиться в пуле до закрытия. По истечении соединение удаляется. По умолчанию 90 секунд.
Развёрнутый ответ
Когда соединение перестало использоваться (последний ответ прочитан и тело закрыто), оно помещается в пул idle. Если за IdleConnTimeout не поступит нового запроса к тому же хосту, транспорт посчитает соединение "устаревшим" и закроет его. Это освобождает ресурсы клиента и сервера. Слишком малое значение теряет преимущества keep-alive; слишком большое — может привести к накоплению полуоткрытых соединений (если сервер или NAT уже разорвали). Оптимально чуть меньше, чем серверный keep-alive timeout или таймаут промежуточного устройства (обычно 60-90с). Можно настроить на уровне транспорта:
transport := &http.Transport{
IdleConnTimeout: 30 * time.Second,
}
На стороне сервера аналог — http.Server.IdleTimeout, который определяет, сколько ждать следующего запроса по keep-alive, прежде чем сервер закроет соединение.
Под капотом (internals)
В Transport.connsPerHost структура persistConn при помещении в пул запускает таймер idleTimer на величину IdleConnTimeout. Если он срабатывает, и соединение всё ещё в пуле, вызывается closeConnIfStillIdle (закрывает). При извлечении соединения из пула таймер останавливается. Сброс таймера происходит при каждом возврате. Параметр также учитывается при массовой очистке CloseIdleConnections() — соединения удаляются независимо.
Типичные ошибки кандидатов
-
Установка
IdleConnTimeout=0(бесконечность)
Кажется, что хотим держать соединения вечно, но тогда мёртвые соединения никогда не убираются, что при нестабильной сети ведёт к ошибкам при использовании.0означает без таймаута. Лучше всегда ставить явное значение. -
Смешивание с TCP Keep-Alive и ожидание, что IdleConnTimeout обнаружит разрыв
IdleConnTimeoutдействует только на уровне приложения и не посылает пробные пакеты. Мёртвое соединение может оставаться в пуле до его использования (и ошибки). Нужно комбинировать с TCP keep-alive черезnet.Dialer.KeepAlive. -
Не учитывать, что сервер может закрыть раньше
Если серверныйIdleTimeout= 60с, а клиентскийIdleConnTimeout= 120с, то половину времени в пуле будут висеть соединения, которые сервер уже закрыл, но клиент ещё не знает. При попытке использования получим "connection reset". Значение клиента должно быть чуть меньше серверного. -
Менять
IdleConnTimeoutна лету у используемого транспорта
Это небезопасно без синхронизации, так как старые таймеры уже запущены с предыдущим значением. Лучше создать новый транспорт или выполнить безопасную замену черезatomic.
Возможные дополнительные вопросы интервьюера
-
Как IdleConnTimeout влияет на HTTP/2?
В HTTP/2 одно соединение мультиплексируется, idle timeout применим ко всему соединению. Если нет активных потоков, транспорт закрывает его по таймауту, что может быть дороже из-за пересоздания мультиплексного соединения. -
Как узнать, что соединение простаивает из-за этого таймаута?
Использоватьhttptraceс колбэкамиGotConnиPutIdleConn, чтобы видеть жизненный цикл соединения. -
Есть ли аналог для сервера?
Да,http.Server.IdleTimeout— если соединение не получило ни одного байта за это время, сервер его закрывает.
Что такое TLS и как он работает в Go?
Краткий ответ (30–60 секунд)
TLS (Transport Layer Security) — криптографический протокол, обеспечивающий конфиденциальность, целостность и аутентификацию поверх TCP. В Go реализован пакетом crypto/tls, который интегрирован в net/http через http.Server.TLSConfig и http.Transport.TLSClientConfig.
Развёрнутый ответ
TLS работает слоем над TCP. Этапы:
- Handshake: согласование версии, наборов шифров, обмен сертификатами, установление сессионного ключа.
- Данные фрагментируются на записи, шифруются и аутентифицируются с помощью AEAD-шифров (AES-GCM, ChaCha20-Poly1305).
- При закрытии отправляется alert
close_notify.
В Go сервер TLS настраивается так:
server := &http.Server{
Addr: ":443",
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
},
}
server.ListenAndServeTLS("cert.pem", "key.pem")
Клиентская сторона: http.Client использует DefaultTransport.TLSClientConfig; при необходимости создают кастомный транспорт с TLSClientConfig. По умолчанию проверяется цепочка сертификатов; можно задать свои корневые сертификаты (RootCAs), клиентский сертификат.
Go поддерживает TLS 1.0–1.3; TLS 1.3 включён по умолчанию с Go 1.12+ (оптимизированный handshake, 0-RTT). Пакет crypto/tls написан на чистом Go, что даёт контроль и отсутствие зависимостей от системных библиотек (хотя иногда используется cgo для OpenSSL, но не рекомендуется). Также есть пакет crypto/x509 для работы с сертификатами.
Под капотом (internals)
crypto/tls реализует конечный автомат для handshake. Структура Conn оборачивает net.Conn и содержит ридер/врайтер, работающий с записями TLS. Handshake запускается методами Handshake (клиент) и Handshake (сервер) автоматически при первой операции. Ключи сессии вычисляются из мастер-секрета с помощью HKDF. Для HTTP/2 поддержка ALPN реализуется через расширение NextProto. В Go 1.22 добавлены улучшения по производительности (параллельные вычисления сертификатов). Поддержка 0-RTT реализована через tls.Config.ClientSessionCache.
Типичные ошибки кандидатов
-
Использование устаревших версий TLS
Не задаютMinVersion, что позволяет TLS 1.0. В продакшене min TLS 1.2. Также нужно исключать уязвимые cipher suites, хотя Go по умолчанию безопасен. -
Забывают про
InsecureSkipVerifyили используют его в проде
Отключает проверку сертификата — допустимо только для тестов. Для интеграции с самоподписанными сертификатами нужно добавить CA вRootCAs. -
Ошибки при загрузке клиентских сертификатов
Путают PEM-файлы, не указывают пароль для зашифрованного ключа. Правильно:tls.LoadX509KeyPair("cert.pem", "key.pem"). Для encrypted ключа нужно сначала расшифроватьx509.DecryptPEMBlock. -
Неправильный порядок
MinVersionи взаимные протоколы
Если указатьMaxVersionниже минимальной поддерживаемой сервером, рукопожатие провалится. Лучше оставитьMaxVersion=0.
Возможные дополнительные вопросы интервьюера
-
Как настроить mTLS (взаимный TLS)?
На сервереClientAuth: tls.RequireAndVerifyClientCertи предоставитьClientCAsкак пул доверенных CA. На клиенте добавить клиентский сертификат вTLSClientConfig.Certificates. -
Как работает Session Tickets в TLS?
Позволяют клиенту возобновлять сессию без полного повторения обмена ключами, передавая зашифрованный тикет. В GoServerSessionCacheиClientSessionCacheуправляют этим. -
Что делает
GODEBUG=tls13=1?
Включает режим отладки или определённые особенности; для продакшена не требуется.
Как сделать HTTP/2 сервер?
Краткий ответ (30–60 секунд)
В Go HTTP/2 сервер работает "из коробки", если используется TLS: при старте с ListenAndServeTLS сервер автоматически поддерживает HTTP/2 через ALPN. Для не-TLS (h2c) можно использовать golang.org/x/net/http2/h2c. Или сконфигурировать http.Server с TLSConfig.
Развёрнутый ответ
Когда вы запускаете server.ListenAndServeTLS("cert.pem", "key.pem"), стандартная библиотека автоматически регистрирует обработчик для HTTP/2 через http2.ConfigureServer. Это происходит в http.Server.ServeTLS(), которая вызывает http2.ConfigureServer(s, nil). В результате мультиплексируются входящие соединения: после TLS handshake, если клиент объявил поддержку "h2" через ALPN, соединение передаётся во внутренний HTTP/2 сервер. HTTP/1.1 и HTTP/2 работают на одном порту.
Если нужен HTTP/2 без TLS (h2c), нужно использовать x/net/http2 и обёртку h2c.NewHandler:
s := &http.Server{Addr: ":8080", Handler: mux}
http2.ConfigureServer(s, nil) // все равно нужно для некоторых версий
http.ListenAndServe(":8080", h2c.NewHandler(mux, &http2.Server{}))
Под капотом (internals)
В файле server.go при вызове ServeTLS после создания TLS-листенера вызывается http2.ConfigureServer, который устанавливает поля s.TLSNextProto (карта протоколов). В x/net/http2 реализован полный фрейминг: HEADERS, DATA, PRIORITY, RST_STREAM, SETTINGS, PUSH_PROMISE (server push по умолчанию отключён). Каждый стрим обрабатывается как отдельный запрос с ResponseWriter. Поддержка server push может быть выключена из-за низкой утилизации в браузерах. Go 1.22+ перевёл инсталляцию HTTP/2 в стандартную библиотеку, но код всё ещё в x/net/http2.
Типичные ошибки кандидатов
-
Запуск HTTP/2 без TLS на порту и ожидание, что будет работать
Без явной конфигурации h2c, сервер будет обслуживать только HTTP/1.1 даже если клиент попытается обновиться (Upgrade: h2c). Нужно явно использоватьh2c. -
Не настройка
IdleTimeoutдля HTTP/2
Поскольку через одно TCP-соединение проходит много потоков, преждевременное закрытие из-за idle влияет на все потоки. СерверныйIdleTimeoutдолжен быть больше интервала keep-alive пингов, чтобы не разрывать активные соединения с простаивающими стримами. -
Ожидание, что server push улучшит производительность без проверки
Браузеры обычно отключают push, и он может ухудшить latency. В продакшене обычно отключают (http2.Server{MaxConcurrentStreams: ...}) или не используют. -
Изменение
TLSConfig.NextProtosвручную без понимания последствий
Порядок важен:["h2", "http/1.1"]— клиент выберет h2 при возможности. Если оставить только ["http/1.1"], HTTP/2 не будет согласован.
Возможные дополнительные вопросы интервьюера
-
Как отключить HTTP/2 на сервере?
Не вызыватьListenAndServeTLS, а использоватьListenAndServeс TLS-листенером, вручную настроенным без настройкиTLSNextProto, либоhttp2.ConfigureServerне вызывать и пропуститьTLSNextProto. Или задатьTLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}пустую карту. -
Как обрабатывать приоритеты потоков HTTP/2?
Go стандартный сервер игнорирует приоритеты, что допускается спецификацией. Для сложных сценариев надо смотреть в сторону gRPC, который имеет свои механизмы. -
Какие преимущества HTTP/2 для бэкенд-сервисов?
Мультиплексирование избавляет от необходимости открывать множество TCP-соединений, снижая overhead и улучшая управление ресурсами; двунаправленный стриминг через одно соединение (gRPC).
Как работает x/net/http2?
Краткий ответ (30–60 секунд)
Пакет golang.org/x/net/http2 реализует протокол HTTP/2 для Go. Он содержит http2.Transport и http2.Server, интегрирующиеся с net/http через http.Transport.TLSNextProto. Бинарное фреймирование, мультиплексирование потоков, управление потоком на уровне стримов и соединения, HPACK-сжатие заголовков.
Развёрнутый ответ
Ключевые типы:
http2.Server— обрабатывает фреймы от клиента, управляет потоками, вызываетhttp.Handler.http2.Transport— round tripper, устанавливающий HTTP/2 соединения. Поддерживает приоритеты (отправляет, но сервер Go их игнорирует).http2.Framer— чтение/запись фреймов поio.ReadWriter.http2.ClientConn/serverConn— управление состоянием одного TCP-соединения.
HTTP/2 использует бинарные фреймы: типа SETTINGS, HEADERS, DATA, RST_STREAM, PING, GOAWAY, WINDOW_UPDATE. Flow control реализован через окно размером 64K по умолчанию, динамически изменяемое через WINDOW_UPDATE.
Взаимодействие с net/http: при настройке http.Transport вызывается http2.ConfigureTransport, которая задаёт TLSNextProto["h2"], перехватывая TLS-соединение и создавая http2.ClientConn.
Под капотом (internals)
Серверная часть: при получении TLS-соединения с ALPN "h2", функция nextProto создаёт http2.serverConn, который запускает чтение фреймов в цикле. Каждый приходящий HEADERS фрейм открывает новый stream, который оборачивается в http2.requestBody и http2.responseWriter. Структура responseWriter реализует http.ResponseWriter и дополнительно Flusher и Pusher. Сжатие HPACK реализовано в пакете x/net/http2/hpack.
Транспортная часть: http2.Transport.RoundTrip берёт существующее ClientConn из кэша или создаёт новое. Затем создаётся clientStream, записываются заголовки, данные, и ожидается ответ. Пинги и GOAWAY обрабатываются в фоновых горутинах.
Типичные ошибки кандидатов
-
Неявное предположение, что
x/net/http2не нужен при использовании стандартногоnet/http
Стандартныйnet/httpвключает HTTP/2 через тот же самыйx/net/http2(вендоринг). Явное импортирование может привести к конфликту версий. Обычно не нужно вручную импортировать, кроме случаев кастомизации. -
Игнорирование настройки
MaxConcurrentStreams
По умолчанию сервер ограничивает 250 одновременных потоков на соединение. Если сервис использует долгие стримы (long polling, SSE), можно исчерпать лимит. Следует настраиватьhttp2.Server.MaxConcurrentStreamsвверх. -
Путаница с
GETи body в HTTP/2
HTTP/2 позволяет тело в любых методах, ноnet/httpможет молча игнорировать или запрещать. Не стоит полагаться на это; лучше не отправлять тело в GET. -
Неправильная обработка RST_STREAM
При вызовеw.WriteHeader(500)после того как заголовки уже отправлены, может возникнуть RST_STREAM. Клиенты могут получать разрыв вместо ошибки.
Возможные дополнительные вопросы интервьюера
-
Как отключить flow control на сервере?
Нельзя полностью отключить, но можно выставить очень большие значенияhttp2.Server.MaxUploadBufferPerStreamиhttp2.Server.MaxUploadBufferPerConnection, чтобы минимизировать блокировки. -
Что такое приоритеты и как они влияют на порядок ответов?
Клиент может присвоить приоритет потокам; сервер Go их игнорирует, но gRPC-Go может использовать для определения порядка отправки ответов. -
Как включить server push?
http2.Serverимеет полеIdleTimeout, при использованииPusherинтерфейсаw.(http.Pusher).Push(...), но push должен быть разрешён и в конфигурации (http2.Server).
Как работает http.Request и http.ResponseWriter?
Краткий ответ (30–60 секунд)
http.Request представляет входящий HTTP-запрос: метод, URL, заголовки, тело, контекст. Создаётся сервером при разборе сырых данных. http.ResponseWriter — интерфейс для формирования ответа: запись заголовков, статуса и тела. Конкретные реализации зависят от протокола (HTTP/1.1, HTTP/2).
Развёрнутый ответ
http.Request содержит:
Method,URL(разобранный URL),Proto(HTTP/1.1),Header(map[string][]string),Body(io.ReadCloser — обёртка над TCP-соединением),Host,RemoteAddr,RequestURI.Context()— возвращает контекст запроса, который можно отменить клиентом или сервером.Form,PostForm— заполняются послеParseForm().MultipartForm— послеParseMultipartForm().TLS— информация о TLS-соединении.Response— nil для входящих, ссылка на ответ при использовании HTTP-редиректов.
Сервер наполняет эти поля после парсинга строки запроса и заголовков. Тело Body может быть лимитировано MaxBytesReader.
http.ResponseWriter — интерфейс:
type ResponseWriter interface {
Header() http.Header
Write([]byte) (int, error)
WriteHeader(statusCode int)
}
Первый вызов WriteHeader фиксирует статус; если не вызван, по умолчанию 200 OK. Заголовки, установленные через Header().Set(), должны быть записаны до WriteHeader или Write. Поддерживаются дополнительные интерфейсы: http.Flusher (немедленная отправка буфера), http.Hijacker (перехват TCP-соединения), http.Pusher (server push для HTTP/2). В Go 1.20+ добавлен http.ResponseController для установки дедлайнов и т.п., не привязываясь к конкретному типу.
Под капотом (internals)
При поступлении запроса server.conn.readRequest() создаёт http.response (приватный тип, реализующий ResponseWriter), который содержит bufio.Writer для буферизованной записи. Ответ пишется в обратном порядке: заголовки, статус-код, тело. В HTTP/2 используется http2.responseWriter, совместимый с ResponseWriter. Для чтения тела запроса используется body структура, обёртывающая io.LimitedReader, если у сервера установлен MaxHeaderBytes.
Типичные ошибки кандидатов
-
Вызов
WriteHeaderи последующая попытка изменить заголовки
Бесполезно, заголовок не попадёт в ответ. Ошибка выявляется при тестировании: ответ не содержит ожидаемого заголовка. Решение: установить все заголовки до вызоваWriteHeader. -
Чтение
r.Bodyнесколько раз без буферизации
Body— поток, читается один раз. Если middleware прочитает тело для аудита, обработчик его не получит. Используйтеio.NopCloser(bytes.NewReader(...))или сохраняйте в контексте. -
Игнорирование возвращаемого значения
Write
Непроверка ошибки записи может привести к тому, что клиент не узнает об обрыве. Ошибка может означать отключение клиента. Лучше логировать, но при ошибке продолжать обработку бессмысленно. -
Ошибочное использование
w.Writeдля маленьких порций, вызывая много передач
КаждыйWriteможет приводить к отдельному TCP-сегменту и фрагментированному HTTP-ответу. Лучше собрать тело в буфер (bytes.Buffer) и записать одним вызовом.
Возможные дополнительные вопросы интервьюера
-
Как правильно установить куки?
Черезhttp.SetCookie(w, &http.Cookie{...}), что добавляетSet-Cookieзаголовок. Можно и вручную вHeader().Add("Set-Cookie", ...). -
Что делает
http.Error(w, "Not Found", 404)?
Устанавливает соответствующий заголовок Conent-Type: text/plain, пишет статус и тело с сообщением. Простой способ вернуть ошибку. -
Как реализовать Hijack для WebSocket?
Проверить, чтоwреализуетhttp.Hijacker; вызватьhijacker.Hijack(), получитьnet.Connи далее общаться по WebSocket протоколу.
Что такое http.Handler?
Краткий ответ (30–60 секунд)
http.Handler — интерфейс с единственным методом ServeHTTP(http.ResponseWriter, *http.Request). Любая структура, реализующая его, может обрабатывать HTTP-запрос. Это центральная абстракция обработки входящих запросов в Go.
Развёрнутый ответ
Интерфейс:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Мультиплексор http.ServeMux сам реализует Handler, делегируя зарегистрированным обработчикам. Можно напрямую передать свой Handler в http.ListenAndServe. Функцию можно превратить в обработчик через http.HandlerFunc(f). Вся цепочка middleware строится на замыкании этого интерфейса.
Простота позволяет компоновать поведение: оборачивая один Handler другим, получаем расширяемость без наследования. Пример:
type MyHandler struct{}
func (h MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello")
}
Под капотом (internals)
В serverHandler.ServeHTTP (приватном) мультиплексор или пользовательский обработчик вызывается после построения response. Никакой особой магии — вызов виртуального метода. Именно благодаря такому дизайну легко внедрять промежуточные слои. Контекст запроса доступен через r.Context(), его отмена корректно пробрасывается.
Типичные ошибки кандидатов
-
Создание
Handler, который блокирует горутину без контекстной отмены
ЕслиHandlerвыполняет долгую операцию и не слушаетr.Context().Done(), горутина останется висеть при разрыве клиента. Надо периодически проверять контекст. -
Попытка написать ответ после возврата из
ServeHTTP(запуск горутины)
ПередачаResponseWriterв другую горутину некорректна: после возвратаServeHTTPфреймворк считает ответ завершённым и может закрыть соединение или повторно использовать. Исключение — hijack. -
Реализация
Handler, которая паникует без recovery
Если не обёрнута в recovery middleware, паника обрушит весь процесс. Всегда используйтеrecoverв верхнем уровне. -
Ошибочное смешивание
http.HandlerFuncиhttp.Handler
http.HandlerFunc— адаптер типа функции. Путают, передавая функцию напрямую в переменную, ожидающуюHandler, но компилятор помогает.
Возможные дополнительные вопросы интервьюера
-
Можно ли использовать
http.ListenAndServe(":80", nil)?
Да, тогда используетсяDefaultServeMux. Не рекомендуется в проде из-за проблем безопасности. -
Как применить разные middleware к разным группам маршрутов?
Создать несколькоServeMux, каждый обёрнутый нужным middleware, и скомбинировать их на верхнем уровне. -
Как
Handlerсвязан сhttp.ServeHTTP?
Handler— интерфейс,ServeHTTP— его метод.http.ServeHTTP— функция-шорткат для быстрого создания обработчика.
Как реализовать ServeHTTP?
Краткий ответ (30–60 секунд)
Реализовать метод ServeHTTP(w http.ResponseWriter, r *http.Request) у своего типа, который удовлетворяет интерфейсу http.Handler. Внутри читаем r, формируем ответ, записываем заголовки и тело через w. Либо использовать http.HandlerFunc с готовой функцией.
Развёрнутый ответ
Пример:
type HelloHandler struct {
Message string
}
func (h *HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK) // можно опустить
fmt.Fprintf(w, "%s\n", h.Message)
}
Функциональный вариант:
http.Handle("/hello", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// ...
}))
При реализации необходимо учитывать:
- Не блокироваться на неопределённый срок, слушать контекст.
- Не записывать заголовки после
WriteHeader. - Корректно обрабатывать ошибки записи.
- Для потоковых ответов реализовывать
Flusher.
Под капотом (internals)
После того как сервер разобрал запрос, он вызывает h.ServeHTTP(rw, req). rw — это http.response с буферизованным writer. При вызове Write данные попадают в буфер, при заполнении или явном Flush отправляются клиенту. При выходе из ServeHTTP сервер проверяет, был ли записан Content-Length или использовано чанковое кодирование, и выполняет завершающие действия (закрытие или keep-alive).
Типичные ошибки кандидатов
-
Изменение
r.URLилиr.Methodбез копии
Это может повлиять на последующую обработку в middleware, т.к. указатель на один и тот же Request разделяется. Лучше не изменять. -
Забывают обработать ошибку
Write
Если запись вwнеудачна, дальнейшие действия бессмысленны, но обработка продолжается. Лучше вернуться и не делать дополнительной работы. -
Злоупотребление
panicвнутриServeHTTP
Паника приведёт к обрыву соединения с некрасивым завершением. Нужно перехватывать и возвращать HTTP 500 осмысленно. -
Игнорирование
r.Context()при запуске горутин
Запущенные горутины могут продолжить работу после отмены запроса, если не передать контекст и не проверять его.
Возможные дополнительные вопросы интервьюера
-
Как в
ServeHTTPопределить, завершил ли клиент соединение?
Проверитьr.Context().Err() != nilили использоватьhttp.ResponseControllerдля установки дедлайнов на чтение/запись. -
Как добавить middleware к моему обработчику без изменения кода
ServeHTTP?
Обернуть его:midware(myHandler). -
Можно ли в одном
ServeHTTPобрабатывать несколько запросов?
Нет, одна горутина на одно соединение в HTTP/1.1 обрабатывает последовательно, но для каждого запроса вызываетсяServeHTTPзаново.
Как обрабатывать таймауты (ReadTimeout, WriteTimeout, IdleTimeout)?
Краткий ответ (30–60 секунд)
На сервере устанавливают поля http.Server: ReadTimeout — время на чтение всего запроса (включая тело); WriteTimeout — время на запись ответа; IdleTimeout — время ожидания следующего запроса при keep-alive. Эти таймауты защищают от медленных клиентов и утечек ресурсов.
Развёрнутый ответ
Пример конфигурации:
srv := &http.Server{
Addr: ":8080",
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
ReadTimeoutначинает отсчёт от момента принятия соединения до полного прочтения запроса (включая тело). Если клиент шлёт заголовки медленно, соединение закроется.WriteTimeoutохватывает запись ответа от первых заголовков до конца тела. Если запись не укладывается, соединение закрывается.IdleTimeout— после отправки ответа сервер ждёт следующий запрос на том же соединении. По истечении закрывает.
В Go 1.20 появился http.ResponseController для установки дедлайнов на уровне отдельного ответа (например, продлить запись для большого файла), но серверные таймауты всё равно действуют как верхний предел.
Под капотом (internals)
Сервер создаёт conn со счётчиками таймаутов через net.Conn методы SetDeadline, SetReadDeadline, SetWriteDeadline. Таймауты переводятся в соответствие с текущим состоянием конечного автомата соединения. Если клиент завис во время чтения тела, readRequest вернёт ошибку i/o timeout, и соединение будет закрыто. Для WriteTimeout используется deadline на весь период записи ответа. Если обработчик превышает WriteTimeout при написании ответа, соединение обрывается, но горутина может продолжать работу — это надо учитывать.
Типичные ошибки кандидатов
-
Установка только
ReadTimeoutбезWriteTimeout
Злоумышленник может открыть соединение и медленно читать ответ, забивая ресурсы.WriteTimeoutпредотвращает вечно висящие записи. -
Неверное понимание, что
WriteTimeoutвключает и ожидание генерации ответа
Да, время от начала чтения первого байта запроса до завершения записи ответа, включая время работы обработчика. Если обработчик долгий, нужно увеличивать таймаут или переносить работу в фон с ранним ответом. -
Игнорирование
IdleTimeoutдля WebSocket
Для WebSocket idle-таймаут сервера должен быть отключён или очень большим, иначе он закроет соединение во время простоя. Обычно для WebSocket используют отдельныйhttp.ServerсIdleTimeout=0. -
Изменение таймаутов на лету без перезапуска сервера
ПоляServerнебезопасны для изменения после старта. Нужно пересоздавать сервер или использовать сигналы для graceful перезапуска.
Возможные дополнительные вопросы интервьюера
-
Как установить динамический таймаут для конкретного запроса?
Использоватьhttp.ResponseController.SetWriteDeadline(deadline)илиSetReadDeadlineв обработчике, но это не отменяет общиеServer.ReadTimeout, а лишь может установить более жёсткий лимит. -
Что произойдёт при превышении
ReadTimeoutдо получения заголовков?
Соединение будет закрыто, клиент получит ошибку (ECONNRESET или timeout). Если сервер уже частично прочитал, ответа не будет. -
Как обрабатывать медленных клиентов при отправке большого файла?
Использоватьhttp.ServeContentс поддержкой range-запросов и установкойWriteTimeoutс запасом, либо динамически обновлять deadline черезResponseController.
Как работает context в HTTP запросах?
Краткий ответ (30–60 секунд)
Каждый http.Request содержит контекст (r.Context()), который создаётся сервером при поступлении запроса. Этот контекст отменяется, если клиент разрывает соединение или истекает таймаут сервера. Контекст используется для передачи deadline, значений и отмены операций по цепочке обработки.
Развёрнутый ответ
Сервер создаёт контекст на основе BaseContext (если задан) и обогащает его дедлайном в зависимости от ReadTimeout. Когда соединение разрывается, контекст получает cancel. Обработчик должен проверять ctx.Done() для "долгоиграющих" операций (например, БД, внешние API). Также контекст используется для передачи request-scoped значений (request ID, trace info).
Клиентская сторона: http.Request может быть создан с http.NewRequestWithContext, и RoundTrip гарантирует, что если контекст будет отменён, запрос прервётся (закроется TCP-соединение или прекратится ожидание). Внутренний транспорт использует req.Context() для управления wantConn и т.д.
Под капотом (internals)
При создании http.response на сервере создаётся контекст с функцией отмены, которая связана с conn и состоянием. Структура cancelCtx внутри context пакета привязывается к каналам. Когда соединение закрывается, запускается отмена. В transport контекст запроса обёртывается, и если он отменён до получения ответа, RoundTrip возвращает ошибку context.Canceled. Также организуется race-условие между получением ответа и отменой.
Типичные ошибки кандидатов
-
Игнорирование контекста в обработчике
Не проверяяr.Context().Done(), можно бесконечно ждать БД, занимая горутину и удерживая сокеты, даже если клиент отвалился. Обязательно пробрасывать контекст во все вызовы. -
Сохранение контекста запроса в глобальной переменной или структуре
Контекст жив только во время запроса; его передача "на будущее" приводит к утечкам и неопределённому поведению. Для связи между запросами использовать другие механизмы. -
Извлечение значений из контекста без типизации ключей
context.WithValueлучше использовать с неэкспортируемыми типами ключей, чтобы избежать коллизий. Строковые ключи могут конфликтовать с другими пакетами. -
Отмена контекста в клиентском коде без учета возможного дрейн-тела
Если контекст отменён, но ответ уже получен, клиентский код может пропустить закрытие тела, и соединение не вернётся в пул.
Возможные дополнительные вопросы интервьюера
-
Как передать trace ID через контекст?
В middleware:ctx = context.WithValue(r.Context(), traceIDKey, id); r = r.WithContext(ctx). -
Что делает
http.TimeoutHandler?
ОборачиваетHandler, устанавливая таймаут на выполнение обработчика; при превышении возвращает 503. Основан на контексте: создаёт дочерний контекст с deadline. -
Как в клиенте прервать запрос по таймауту?
Создать контекст с таймаутом:ctx, cancel := context.WithTimeout(parent, 5*time.Second); defer cancel(); req, _ := http.NewRequestWithContext(ctx, "GET", url, nil).
Как отменить HTTP запрос?
Краткий ответ (30–60 секунд)
Отменить HTTP-запрос можно через отмену контекста, переданного в запрос: ctx, cancel := context.WithCancel(...), передать в http.NewRequestWithContext. Вызов cancel() прервёт запрос на транспортном уровне (закроет TCP-соединение или перестанет ждать). Альтернативно, использовать http.Client.Timeout.
Развёрнутый ответ
Клиентская отмена:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", "http://example.com", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
// мог быть context.DeadlineExceeded или Canceled
}
Транспорт реализует отмену через закрытие persistConn или отмену ожидания в пуле. Для TCP-соединения устанавливается дедлайн на чтение, так что блокировка снимается.
Серверная сторона: использовать req.Context().Done() для досрочного выхода из обработчика. Но сам запрос сервером не отменяется, только обработка может быть остановлена.
Под капотом (internals)
В transport.roundTrip создаётся requestCanceler, который при отмене контекста вызывает cancelRequest на persistConn (если уже ассоциировано). persistConn закрывает writeLoop и readLoop, что приводит к разрыву TCP. Если запрос ожидает в очереди wantConn, отмена извлекает его из очереди.
Типичные ошибки кандидатов
-
Вызов
cancelдо проверки ошибкиDo
Еслиcancelотложен сdefer cancel(), то послеDoконтекст всё равно отменяется, что может преждевременно закрыть соединение, если вы ещё не прочитали тело ответа. Лучше вызыватьcancelпосле того, как тело прочитано. -
Использование
Client.Timeoutвместе с контекстом без понимания суммирования
Client.Timeoutоборачивает время на весьDo, включая редиректы. Если контекст имеет свой deadline, они действуют оба, и сработает более ранний. -
Не проверка
errпослеDoнаcontext.Canceled
Часто путают с ошибкой сети, а это может быть отмена по таймауту. Нужно различать для метрик и ретраев. -
Забывают, что
http.NewRequestне привязывает контекст
Использованиеhttp.NewRequestбез контекста создаёт запрос сcontext.Background(), который никогда не отменяется. Нужно всегда использоватьNewRequestWithContext.
Возможные дополнительные вопросы интервьюера
-
Как отменить запрос на стороне сервера?
Контекст запроса отменяется при разрыве TCP-соединения. Сервер может принудительно отменить черезhttp.ResponseController.SetReadDeadline(на чтение тела), но сам запрос всё равно выполнится. -
Что такое
http.Transport.CancelRequest?
Устаревший метод для отмены запросов по ключу (до введения контекстов). Не рекомендуется, используйте контекст. -
Влияет ли отмена на keep-alive соединение?
При отмене транспорт старается закрыть соединение или не возвращать его в пул, чтобы избежать неконсистентного состояния.
Как работает net.Dial?
Краткий ответ (30–60 секунд)
net.Dial устанавливает сетевое соединение с указанным адресом по протоколу (tcp, udp, unix). Возвращает net.Conn. Использует системные вызовы socket -> connect (для TCP). Может применять net.Dialer с таймаутами, локальным адресом, резолвером.
Развёрнутый ответ
Простой вызов:
conn, err := net.Dial("tcp", "golang.org:80")
Внутри: парсится адрес; если нужно, выполняется DNS-резолвинг через net.Resolver; для каждого IP-адреса пытается установить соединение, может пробовать несколько (happy eyeballs). Для TCP создаётся сокет, вызывается connect(), после успеха возвращается *net.TCPConn.
net.Dialer позволяет детально управлять:
Timeout— общее время на установку соединения.Deadline— абсолютное.LocalAddr— исходный адрес.KeepAlive— включение TCP keep-alive и период.Control— функция, получающая файловый дескриптор и позволяющая настраивать параметры сокета (SO_REUSEPORT, SO_MARK).Resolver— кастомный резолвер.
Под капотом (internals)
net.Dialer.DialContext принимает контекст; создаёт неблокирующий сокет, начинает асинхронное соединение, ожидая через сетевой поллер или блокируясь в горутине. Используются системные вызовы: socket, bind (если LocalAddr), connect. Сетевой поллер (epoll/kqueue) позволяет эффективно ждать завершения соединения без привязки потока. При использовании с net.Dial напрямую блокируется горутина, но Go runtime переводит её в ожидание.
Типичные ошибки кандидатов
-
Использование
net.Dialбез контекста в современных приложениях
Нет возможности отмены или таймаута, кроме Timeout Dialer. Лучше всегда использоватьDialer.DialContext. -
Путаница с тем, что
net.Dial("tcp", "localhost:80")может подключаться по IPv6
Если хост резолвится в IPv6 и IPv4, реализация happy eyeballs пробует оба адреса; может выбрать не тот, что ожидали. Если нужно явно IPv4, используйте"tcp4". -
Невозможность переиспользовать
connпосле ошибки
ЕслиDialвернул ошибку,connравен nil. Не пытайтесь его закрыть (паника). -
Игнорирование настройки
KeepAliveпри длительных соединениях
По умолчанию TCP keep-alive может быть отключён или с интервалом часы. NAT и файрволы быстро обрывают "тихие" соединения. УстановитеKeepAlive: 30 * time.Second.
Возможные дополнительные вопросы интервьюера
-
Как
Dialerреализует Happy Eyeballs?
Пытается подключиться к адресам разных семейств параллельно, выигрывает первый успешный, другой закрывается. Реализовано вdial.goчерез каналы. -
Как обернуть
net.Connдля проксирования?
ИспользоватьDialer.Control? Не совсем; для прокси обычно нужен CONNECT, это делаетhttp.Transport. -
Чем отличается
net.DialTimeout?
Устаревший шорткат: создаётDialerс Timeout и вызываетDial.
Что такое net.Conn?
Краткий ответ (30–60 секунд)
net.Conn — интерфейс, представляющий двусторонний поток данных (TCP-соединение, UDP, Unix domain socket). Реализует io.Reader, io.Writer, io.Closer, а также методы LocalAddr, RemoteAddr, SetDeadline, SetReadDeadline, SetWriteDeadline. Это фундамент всей сетевой работы в Go.
Развёрнутый ответ
Интерфейс:
type Conn interface {
Read(b []byte) (n int, err error)
Write(b []byte) (n int, err error)
Close() error
LocalAddr() Addr
RemoteAddr() Addr
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
}
Конкретные реализации: *net.TCPConn, *net.UDPConn, *net.UnixConn, *tls.Conn. net.Dial и net.Listen возвращают Conn. Hi-jacking в net/http даёт доступ к Conn, что позволяет реализовать WebSocket или нестандартные протоколы.
Таймауты (deadline) применяются на уровне файлового дескриптора, и по их истечению Read/Write возвращают os.ErrDeadlineExceeded.
Под капотом (internals)
net.Conn — это обёртка над netFD (сетевой файловый дескриптор), которая использует платформо-зависимые syscall. Read и Write сопряжены с горутинами и сетевым поллером: когда сокет не готов, горутина паркуется и пробуждается при событии. Установка deadline преобразуется в таймеры и использует SetDeadline на уровне netFD. Закрытие запускает shutdown и закрытие дескриптора.
Типичные ошибки кандидатов
-
Забывают про короткие чтения в
stream-ориентированном мире
TCP — поток,Readможет вернуть меньше байт, чем длина буфера. Необходимо накапливать данные до полного сообщения. -
Неправильная обработка
io.EOF
При закрытии соединения со стороны удалённого хостаReadвозвращаетio.EOF. Это штатная ситуация, а не ошибка. Кандидаты часто трактуют как критический сбой. -
Попытка писать в закрытое соединение без проверки ошибки
После возвратаio.EOFчтение закрыто, но запись может ещё какое-то время проходить (полузакрытое состояние), что приводит к SIGPIPE в Unix или ошибке. Нужно корректно закрыть. -
Установка дедлайнов и забывание их сбросить
Deadline абсолютный; если вы хотите продлить операцию, нужно установить новый deadline. Без сброса следующая операция может сразу провалиться.
Возможные дополнительные вопросы интервьюера
-
Как скопировать данные между двумя
net.Connэффективно?
Использоватьio.Copyв двух горутинах, возможно сio.Pipeили напрямую. Следить за закрытием. -
Чем отличается
CloseотCloseWriteв TCP?
Closeзакрывает обе стороны.CloseWriteзакрывает только отправку (FIN), позволяя принимать данные. В Go у*net.TCPConnесть методыCloseRead,CloseWrite. -
Можно ли привести
net.Connк*net.TCPConnи зачем?
Да, черезconn.(*net.TCPConn). Позволяет вызывать специфичные методы:SetKeepAlive,SetNoDelay.
Как работает net.Listen?
Краткий ответ (30–60 секунд)
net.Listen создаёт слушающий сокет, привязывается к адресу и начинает принимать входящие соединения, возвращая net.Listener. Вызов Accept() на листенере блокируется до нового соединения и возвращает net.Conn. Close() останавливает приём.
Развёрнутый ответ
Пример:
ln, err := net.Listen("tcp", ":8080")
if err != nil { ... }
for {
conn, err := ln.Accept()
if err != nil { ... }
go handle(conn)
}
Listen делает системный вызов socket, bind, listen. Внутренне net.Listener (интерфейс) реализован *net.TCPListener. Accept() вызывает accept(2), который блокирует до прихода SYN. При закрытии листенера Accept возвращает ошибку (обычно net.ErrClosed).
Для плавной остановки: закрыть листенер, затем дождаться завершения активных соединений.
Под капотом (internals)
При вызове Listen создаётся netFD, устанавливаются опции сокета (SO_REUSEADDR по умолчанию в некоторых ОС). Системный вызов listen устанавливает backlog. Accept использует внутренний поллер для эффективного ожидания. Когда новая горутина вызывает Accept, и нет входящих, она паркуется до события. Это позволяет обслуживать множество листенеров в одном потоке.
Типичные ошибки кандидатов
-
Запуск сервера с
Listen("tcp", ":http")и неожидаемый отказ из-за занятого порта
Проверять ошибку, иногда порт занят; использоватьSO_REUSEPORTдля нескольких процессов. -
Утечка горутин из-за неправильного выхода из цикла приёма
Если при ошибкеAccept(кроме закрытия) продолжают без паузы, могут заспамить лог и процессор. Надо отличать временные ошибки иErrClosed. -
Некорректное закрытие листенера
Закрытие листенера не закрывает существующиеconn. Надо реализовать graceful shutdown: закрыть листенер, дождаться обработки существующих. -
Использование
Listenна том же адресе из нескольких процессов без SO_REUSEPORT
Может не работать (address already in use). Современные ядра поддерживают SO_REUSEPORT для балансировки нагрузки.
Возможные дополнительные вопросы интервьюера
-
Как временно остановить приём новых соединений?
Закрыть листенер. Или использовать семафор, который приостанавливает цикл, ноAcceptвсё равно будет создаватьconnв backlog. -
Что такое backlog?
Максимальная очередь ожидающих соединений, которые ещё не приняты черезAccept. По умолчанию зависит от ОС, можно настроить черезlisten(fd, backlog)вControl. -
В чем разница между
ListenиListenPacket?
ListenPacketдля UDP, не создаёт потоковых соединений.
Как написать TCP сервер без net/http?
Краткий ответ (30–60 секунд)
Использовать net.Listen("tcp", ":port") для создания листенера, в цикле принимать соединения listener.Accept(), получать net.Conn, и в отдельной горутине обрабатывать протокол: читать из conn.Read(), писать в conn.Write(). Реализовать свой формат сообщений.
Развёрнутый ответ
func main() {
ln, _ := net.Listen("tcp", ":9000")
defer ln.Close()
for {
conn, _ := ln.Accept()
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
defer conn.Close()
buf := make([]byte, 1024)
for {
n, err := conn.Read(buf)
if err != nil { break }
// echo
conn.Write(buf[:n])
}
}
Это основа. Для производственных систем нужно: обработка фреймов (признак конца сообщения), таймауты, graceful shutdown, control над числом горутин, буферизация.
Под капотом (internals)
Вся работа с net.Conn напрямую использует системные вызовы. Можно управлять буферами TCP через setsockopt в Control. Например, установить TCP_NODELAY: tcpConn.SetNoDelay(true). Также можно задавать keep-alive. Поскольку вы работаете без net/http, вы получаете полный контроль над потоком и экономите накладные расходы парсинга HTTP.
Типичные ошибки кандидатов
-
Отсутствие протокола: предположение, что одно
Readравно одному сообщению
TCP — потоковый, сообщения могут склеиваться или разбиваться. Нужно вводить фрейминг (длина + данные, разделитель). -
Не обрабатывать частичные записи
Writeможет записать не все данные. Нужно использоватьio.WriteStringили цикл. -
Запуск горутины на каждое соединение без лимита
Легко исчерпать память и горутины под нагрузкой. Нужен семафор или пул воркеров. -
Игнорирование закрытия
connпри ошибке
Если чтение вернулоio.EOF, это нормально; стоит закрытьconn. При других ошибках часто тоже закрывают.
Возможные дополнительные вопросы интервьюера
-
Как обработать тысячи соединений с малым количеством горутин?
Использовать сетевой поллер напрямую (через syscall) или асинхронный ввод-вывод в стилеgnet, но стандартныйnetуже эффективен. -
Как передать данные между соединениями (чат)?
Хранить map соединений, защищать мьютексом, при чтении от одного — писать всем. -
Как тестировать такой сервер?
Создавать клиент черезnet.Dialв тесте, отправлять данные, проверять ответ.
Как обрабатывать WebSocket (gorilla/websocket)?
Краткий ответ (30–60 секунд)
С помощью gorilla/websocket на сервере: создать Upgrader, в HTTP-обработчике вызвать upgrader.Upgrade(w, r, nil), чтобы получить *websocket.Conn. Затем запустить цикл чтения (ReadMessage) и записи (WriteMessage). Клиентская сторона использует websocket.Dial.
Развёрнутый ответ
Серверный фрагмент:
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil { ... }
defer conn.Close()
for {
msgType, msg, err := conn.ReadMessage()
if err != nil { break }
// обработка
conn.WriteMessage(msgType, msg)
}
}
Клиент:
conn, _, err := websocket.DefaultDialer.Dial("ws://server/ws", nil)
Протокол WebSocket работает поверх HTTP upgrade. Соединение переключается на полнодуплексную передачу фреймов (текстовых и бинарных), а также пингов для поддержания keep-alive.
Под капотом (internals)
Библиотека использует http.Hijacker для захвата нижележащего net.Conn после Upgrade. Она реализует фрейминг согласно RFC 6455, управляет маскированием, фрагментацией. Чтение и запись идут в отдельных горутинах внутри ReadMessage/NextReader и WriteMessage. Пинги обрабатываются автоматически, если настроен PingHandler. Поддержка compression (permessage-deflate) включается опционально.
Типичные ошибки кандидатов
-
Блокировка обработчика без использования горутин для записи
Если отправка блокируется из-за медленного клиента, чтение останавливается, и сервер теряет возможность получать сообщения. Нужно разделять: одна горутина читает, другая пишет (канал). -
Не настроена проверка Origin
По умолчанию запрещены все источники, нужно явно разрешить, иначе 403. -
Забывают обрабатывать пинги вручную, когда не используется
ReadMessage
Контрольные фреймы требуют ответа; библиотека по умолчанию отвечает, но если цикл чтения не вызывается часто, соединение может порваться. -
Хранение
*websocket.Connбез синхронизации, конкурентная запись
WriteMessageне потокобезопасен. Нужно либо использовать мьютекс, либо канал, в который пишут несколько отправителей, а единственная горутина производит запись.
Возможные дополнительные вопросы интервьюера
-
Чем отличается WebSocket от Server-Sent Events?
SSE — односторонняя передача сервер -> клиент поверх HTTP, WebSocket — двусторонняя. -
Как обрабатывать большое количество WebSocket соединений?
Использовать эффективный epoll (gorilla/websocket работает на стандартных горутинах), возможно,gobwas/wsдля zero-copy, контролировать память и лимиты. -
Как обойти прокси с WebSocket?
Использоватьwss://(TLS), чтобы прокси не разрывали HTTP upgrade. Или настраивать прокси.
Как работает gRPC?
Краткий ответ (30–60 секунд)
gRPC — фреймворк удалённого вызова процедур, использующий HTTP/2 как транспорт и Protocol Buffers как язык описания и сериализации. В Go генерируется клиентский и серверный код. Сервер реализует интерфейс, клиент вызывает методы как локальные функции. Поддерживает унарные вызовы, стриминг (клиентский, серверный, двунаправленный).
Развёрнутый ответ
Основные шаги:
- Описать сервис в
.protoфайле. - Сгенерировать Go код с помощью
protocс плагинамиprotoc-gen-goиprotoc-gen-go-grpc. - На сервере: создать gRPC сервер (
grpc.NewServer()), зарегистрировать реализацию,Serve(lis). - Клиент:
grpc.Dial(target, opts...), создать клиентский stub, вызывать методы.
Стриминг:
rpc ServerStreaming(Req) returns (stream Resp)— сервер посылает множество ответов.rpc ClientStreaming(stream Req) returns (Resp)— клиент посылает поток запросов.rpc BidiStreaming(stream Req) returns (stream Resp)— обе стороны.
gRPC использует HTTP/2, что даёт мультиплексирование, сжатие заголовков, приоритеты, flow control на уровне стрима.
Под капотом (internals)
В Go gRPC использует google.golang.org/grpc, который построен поверх x/net/http2 и своего транспорта. Сервер создаёт http2.Server и регистрирует обработчик для /service/method. Заголовки и трейлеры передаются как метаданные. Сериализация через proto.Marshal/Unmarshal. Управление соединениями: grpc.ClientConn поддерживает пул соединений, балансировку, retry, keepalive. Interceptors — аналог middleware для перехвата вызовов.
Типичные ошибки кандидатов
-
Игнорирование контекста и deadline
Все RPC методы принимают контекст; если клиент не передаёт таймаут, запрос может зависнуть навсегда. На сервере нужно уважатьctx.Done(). -
Забывают обработать ошибки стрима
При рециве от стримаstream.Recv()может вернутьio.EOF(норма) или ошибку; их нужно различать. -
Не настроены keepalive параметры
Безkeepaliveдолгоживущие стримы могут разрываться промежуточными прокси. Следует задатьgrpc.WithKeepaliveParamsс разумными параметрами. -
Использование синхронных вызовов для стриминга с большими задержками
Клиентский стрим: ожидание завершения отправки наCloseAndRecvможет блокировать горутину; нужно проектировать модель обработки.
Возможные дополнительные вопросы интервьюера
-
Как работает балансировка нагрузки в gRPC?
На стороне клиента черезresolverиbalancer. По умолчанию — pick-first. Можно использовать round-robin:grpc.WithDefaultServiceConfig(...). -
Чем отличаются unary и streaming интерсепторы?
Unary обрабатывают один запрос/ответ, стриминговые оборачивают стрим-объекты. -
Как передать метаданные (аналог заголовков)?
Черезmetadata.New(map...)иmetadata.AppendToOutgoingContext.
В чем преимущества gRPC перед REST?
Краткий ответ (30–60 секунд)
gRPC обеспечивает строгую типизацию (protobuf), автоматическую генерацию клиента/сервера, высокую производительность за счёт бинарной сериализации и HTTP/2, встроенную поддержку стриминга и обратную совместимость контрактов. REST проще для браузеров и отладки, но проигрывает в производительности и отсутствии схемы.
Развёрнутый ответ
Преимущества gRPC:
- Схема (contract-first):
.protoфайлы как источник истины, генерация кода для многих языков. - Эффективность: бинарный Protobuf компактнее JSON, особенно для чисел и вложенных структур. HTTP/2 мультиплексирование снижает latency.
- Стриминг: двунаправленный поток данных из коробки.
- Обратная совместимость: добавление полей без поломки старых клиентов.
- Метаданные: аналог заголовков, но с возможностью бинарных значений.
- Встроенные таймауты/дедлайны, retry, балансировка (через библиотеки).
- Кодогенерация избавляет от ручного написания сериализации/десериализации, снижая ошибки.
REST остаётся удобным для публичных API, кэширования (HTTP кэш), взаимодействия с браузерами, простоты отладки (curl). В микросервисной архитектуре часто выбирают gRPC для внутренних взаимодействий.
Под капотом (internals)
Сравнение производительности: Protobuf сериализация ~ в 5-10 раз быстрее JSON при парсинге в Go, т.к. не требует reflection и выделяет меньше памяти. HTTP/2 мультиплексирование уменьшает количество TCP-соединений, что снижает потребление ресурсов и задержки.
Типичные ошибки кандидатов
-
Считать, что gRPC всегда быстрее при маленьких сообщениях
Накладные расходы HTTP/2 фреймов и сжатия могут быть выше простого HTTP/1.1 с JSON для очень коротких сообщений. Важно профилировать. -
Игнорирование необходимости JSON API для внешних потребителей
Заворачивать gRPC в gRPC-gateway для REST, но не продумав конвертацию ошибок и метаданных. Это усложняет интеграцию. -
Недооценка сложности стриминга
Двунаправленный стриминг требует корректной обработки горутин и backpressure, что не всегда требуется бизнес-логике. -
Забывают про брейкинг-изменения
Несмотря на обратную совместимость Protobuf, переименование полей или пакетов ломает wire-формат.
Возможные дополнительные вопросы интервьюера
-
Как обеспечить REST API из gRPC сервиса?
Использоватьgrpc-gatewayдля генерации reverse-proxy, который транслирует REST в gRPC. -
Какие недостатки у gRPC?
Сложность отладки (нужен grpcurl), отсутствие кэширования, поддержка браузеров через grpc-web, больший размер бинарного кода. -
Как работает дедлайн/таймаут в gRPC и передаётся ли он на сервер?
Да, deadline отправляется в grpc-timeout заголовке; сервер может проверить оставшееся время черезctx.Deadline().
Как работает Protobuf и сериализация?
Краткий ответ (30–60 секунд)
Protocol Buffers — язык описания данных и бинарный формат сериализации. Компилятор protoc генерирует Go структуры с методами Marshal/Unmarshal. Сериализованные данные компактны и обратно совместимы. Protobuf использует теги полей (field numbers) для идентификации в бинарном потоке.
Развёрнутый ответ
Схема:
message Person {
string name = 1;
int32 age = 2;
}
Генерация: protoc --go_out=. person.proto. Полученные структуры реализуют proto.Message. Для кодирования используется формат wire type:
- Varint — для int32, int64, uint, bool, enum.
- Length-delimited — строки, embedded сообщения, bytes.
- 64-bit / 32-bit фиксированной длины.
Каждое поле кодируется как ключ (field_number << 3) | wire_type, за которым следует значение. Благодаря field numbers можно добавлять новые поля, не ломая старых парсеров. Неизвестные поля сохраняются для пересылки (в Go не всегда). В v3 все поля опциональны с нулевыми значениями.
Под капотом (internals)
Реализация в google.golang.org/protobuf (reflect-based, предпочтительный) и старый github.com/golang/protobuf. Marshal проходит через encoder с буфером, записывая varint поля. Unmarshal парсит и заполняет структуру. В Go 1.20+ появились улучшения производительности. При сериализации пустые поля не записываются, что экономит место.
Типичные ошибки кандидатов
-
Изменение field number существующих полей
Это ломает совместимость, т.к. сериализованные данные с старыми номерами будут интерпретированы неверно. Нельзя переиспользовать номера. -
Ожидание, что nil и пустой слайс различаются
В proto3 нет представления nil-поля для слайсов; пустой слайс сериализуется как отсутствие поля. При десериализации будет пустой слайс. -
Неправильное использование
oneof
Полеoneofможет содержать только одно значение; установка другого сбрасывает предыдущее. Забывают обрабатывать это в бизнес-логике. -
Не учитывают, что map — repeated message для wire
Map кодируются как repeated сообщения с key/value полями; порядок итерации не гарантирован.
Возможные дополнительные вопросы интервьюера
-
Что такое gRPC-gateway?
Инструмент генерации REST proxy для gRPC-сервиса; использует обратные аннотации в .proto. -
Как обрабатывать unknown fields в Go?
При маршалинге они игнорируются, но если нужно сохранять, можно использоватьproto.UnmarshalerсDiscardUnknown: false. -
Чем Protobuf лучше JSON для межсервисного общения?
Быстрее, меньше размер, строгая схема, бинарный формат избегает проблем с экранированием.
Как загружать большие файлы через HTTP?
Краткий ответ (30–60 секунд)
Использовать multipart/form-data с потоковой обработкой. В обработчике r.ParseMultipartForm(32 << 20) (лимит памяти), затем r.MultipartForm.File["field"], или более продвинуто: r.MultipartReader() для поточного чтения частей. Файл сохранять на диск или в облачное хранилище чанками, не загружая целиком в память.
Развёрнутый ответ
Пример с MultipartReader:
reader, err := r.MultipartReader()
for {
part, err := reader.NextPart()
if err == io.EOF { break }
if part.FormName() == "file" {
dst, _ := os.Create("upload.dat")
defer dst.Close()
io.Copy(dst, part) // копирует поток, не загружая весь в ОЗУ
}
}
Таким образом файл читается по мере получения. Важно настроить http.Server.MaxHeaderBytes и таймауты (ReadTimeout), чтобы медленные клиенты не занимали ресурсы.
Для очень больших файлов можно совместить с range upload (возобновление), но HTTP сам не поддерживает resume; нужно реализовать на уровне приложения (проверка полученных чанков).
Под капотом (internals)
При multipart чтении net/http использует mime/multipart.Reader, который разбирает границы и выдаёт Part с телом как io.Reader, обёрнутый в io.LimitedReader — лимит размера файла не задан, но можно обернуть самому в io.LimitReader. Данные читаются прямо из r.Body (TCP-соединения), буферизация минимальна.
Типичные ошибки кандидатов
-
Использование
ParseMultipartFormс большим лимитом памяти
Например128 << 20— все файлы загружаются в оперативную память или временные файлы. Лучше обрабатывать потоково. -
Не ограничивают размер загружаемого файла
Злоумышленник может отправить терабайт, израсходовав диск. Обязательно обернутьio.LimitReaderс разумным максимумом. -
Выделение огромного буфера для каждого соединения
Например,io.CopyBufferс гигантским буфером. Использовать дефолтный (32KB) или системный размер. -
Игнорирование ошибок при записи файла
Если диск заполнен, запись прерывается, но клиент может не узнать. Надо прервать чтение и ответить ошибкой.
Возможные дополнительные вопросы интервьюера
-
Как реализовать прогресс загрузки?
На клиенте — использоватьio.TeeReaderдля отчёта о прочитанных байтах. На сервере сложнее, так как стандартныйMultipartReaderне даёт общего прогресса без разбора частей; можно читать сырой поток и парсить границы вручную. -
Как защититься от слишком большого количества запросов?
Rate limiting на уровне соединений, ограничение размера body черезhttp.MaxBytesReader. -
Как обрабатывать многопоточную загрузку одного файла?
Сервер может не поддерживать, нужно реализовать чанки на уровне приложения с координацией.
Как работает докачка файлов (range request)?
Краткий ответ (30–60 секунд)
HTTP поддерживает частичную загрузку через заголовок Range: bytes=start-end. Сервер в ответе со статусом 206 Partial Content возвращает запрошенный фрагмент и заголовки Content-Range, Accept-Ranges. В Go на сервере используется http.ServeContent, который автоматически обрабатывает Range. Клиент может задать Range заголовок при запросе.
Развёрнутый ответ
Клиент:
req, _ := http.NewRequest("GET", url, nil)
req.Header.Set("Range", "bytes=0-1023")
resp, _ := client.Do(req)
if resp.StatusCode == 206 {
body, _ := io.ReadAll(resp.Body)
}
Сервер:
http.HandleFunc("/file", func(w http.ResponseWriter, r *http.Request) {
http.ServeContent(w, r, "filename", fileModTime, fileReaderSeeker)
})
ServeContent автоматически обрабатывает заголовки If-Match, If-None-Match, If-Modified-Since, Range, устанавливает правильные Content-Range, обрабатывает многодиапазонные запросы (Range: bytes=0-100,200-300).
Под капотом (internals)
ServeContent определяет, реализует ли контент io.ReadSeeker; затем разбирает диапазон, устанавливает Content-Range: bytes start-end/total. Если Seeker не доступен, возвращает ошибку. При ошибках в диапазонах отдаёт 416 Range Not Satisfiable. Мультидиапазонные запросы возвращаются с multipart/byteranges.
Типичные ошибки кандидатов
-
Использование статического файлового сервера без
Seeker
Если источник не поддерживаетSeek(например,os.Stdin),ServeContentвернёт ошибку. Надо предоставлятьSeeker. -
Неправильное вычисление длины содержимого
При использовании своих реализаций, важно корректно указыватьContent-Lengthобщий и для диапазона. -
Не обрабатывать If-Range
Условные Range запросы позволяют клиенту возобновить загрузку только если файл не изменился. Игнорирование может привести к повреждённым данным. -
Хранение данных в памяти для раздачи больших файлов
Передачаbytes.Readerна весь файл в памяти — плохо. Используется*os.Fileили кастомная обёртка, реализующаяReadSeeker(например, для объектов в S3 через range запросы к хранилищу).
Возможные дополнительные вопросы интервьюера
-
Как отдавать несколько диапазонов одновременно?
ServeContentподдерживает multiple ranges: устанавливаетContent-Type: multipart/byteranges; boundary=...и пограничные разделители. -
Как обрабатывать запросы на возобновление загрузки после разрыва?
Клиент сохраняет количество принятых байт, шлётRange: bytes=N-. Сервер отвечает с 206. -
Какие статус-коды кроме 206 могут быть?
200 (если Range не поддерживается или запрошен весь файл), 416 (некорректный диапазон).
Как реализовать потоковую передачу (streaming) через HTTP?
Краткий ответ (30–60 секунд)
Потоковая передача — отправка ответа по мере генерации данных, без ожидания полного тела. Достигается: не устанавливать Content-Length (будет chunked Transfer-Encoding), периодически вызывать Flush() для отправки накопленного буфера клиенту. Либо использовать Server-Sent Events (SSE) или long polling.
Развёрнутый ответ
Пример стриминга JSON-объектов:
func streamHandler(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/plain")
for i := 0; i < 10; i++ {
fmt.Fprintf(w, "data: %d\n", i)
flusher.Flush()
time.Sleep(1 * time.Second)
}
}
Клиент читает поток через resp.Body как io.Reader. Сервер автоматически использует chunked кодирование при отсутствии Content-Length. Для отправки больших бинарных данных можно копировать из источника с флашем через io.CopyBuffer и периодический Flush, если буферизация позволяет.
Под капотом (internals)
Когда обработчик пишет в ResponseWriter без установки Content-Length, серверный http.response накапливает данные в буфере (bufio.Writer размером 4КБ). При вызове Flush буфер сбрасывается в TCP-соединение и отправляется чанк (шестнадцатеричный размер, \r\n, данные, \r\n). Клиент видит chunked-ответ. В HTTP/2 используется фрейминг, тоже поддерживается.
Типичные ошибки кандидатов
-
Не проверяют, поддерживается ли
Flusher
При использовании HTTP/2 или нестандартных обёртокResponseWriterможет не реализовыватьFlusher, тогда паника или отсутствие стриминга. Нужно проверятьif f, ok := w.(http.Flusher); ok { f.Flush() }. -
Отсутствие явного
Content-Typeдо первогоFlush
После первогоFlushзаголовки уже отправлены; установкаContent-Typeпозже игнорируется. -
Слишком частый
Flushбез буферизации
Каждый вызовFlushможет порождать маленький TCP-сегмент и заголовок чанка, снижая эффективность. Можно буферизировать небольшие записи вручную. -
Предположение, что клиент может читать с произвольной скоростью
Клиент может быть медленным, что блокирует сервер. Желательно использовать таймауты или контекст для возможности прервать стрим.
Возможные дополнительные вопросы интервьюера
-
Как реализовать бесконечный стрим с heartbeat?
Периодически отправлять комментарии (SSE) или пустые чанки (пробелы), чтобы keep-alive не разорвал соединение. -
Как передавать бинарные данные (например, видео) стримом?
Использоватьio.Copyс кастомным writer, который периодически флашит, или использовать HTTP/2 push. -
Чем chunked encoding отличается от
Content-Length: 0?
Chunked позволяет отправлять тело неизвестного размера; клиент определяет конец по нулевому чанку.
Как работает http.ServeMux и роутинг в Go 1.22+?
Краткий ответ (30–60 секунд)
http.ServeMux — HTTP-мультиплексор, сопоставляющий URL с зарегистрированными Handler. В Go 1.22+ появились улучшения: поддержка HTTP-методов (GET /, POST /) и именованных сегментов пути (/user/{id}), с извлечением через r.PathValue("id"). Нарушение сопоставления теперь приводит к 405 Method Not Allowed или 404.
Развёрнутый ответ
Новый синтаксис:
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
fmt.Fprintf(w, "User %s", id)
})
mux.HandleFunc("POST /users", createUser)
Роутинг базируется на дереве путей, аналогичном pat в стандартной библиотеке. При совпадении шаблона обрабатывается метод. Если путь совпал, но метод не тот, возвращается 405. {name} может ограничиваться регулярным выражением: {id:[0-9]+}. В конце можно указать {$} для точного совпадения без префикса.
PathValue доступен только в обработчике, связанном с конкретным шаблоном. Обработчики по-прежнему можно регистрировать через Handle и Handler.
Под капотом (internals)
Реализовано в net/http через пакет internal/routing (не экспортируется). При регистрации строится дерево радикс-триев с учётом методов. При поступлении запроса мультиплексор ищет наиболее специфичное совпадение. Приоритет: точное совпадение > сегмент с joker {...} > завершающий {$}. Вся логика в ServeHTTP ServeMux. r.PathValue хранит захваченные значения в приватном поле *Request (устанавливается мексом).
Типичные ошибки кандидатов
-
Путаница с
PathValueв middleware
Если middleware оборачиваетServeMux,r.PathValueне доступен на уровне middleware (только внутри зарегистрированного обработчика). Решение: извлекать ID в самом обработчике или использовать контекст. -
Не указан
{$}для конечного пути
Например,GET /users/без {$} будет совпадать с/users/anything, что может быть нежелательно. Надо использоватьGET /users/{$}для точного совпадения. -
Пересекающиеся шаблоны
GET /users/{id}иGET /users/new— порядок регистрации не важен? Мультиплексор решает по приоритету: точное совпадение (static) приоритетнее. Но лучше избегать неоднозначности. -
Использование
HandleFuncбез метода в старом стиле
Если простоmux.HandleFunc("/users", ...), то он сработает для всех методов, что может нарушить REST. Рекомендуется указывать методы.
Возможные дополнительные вопросы интервьюера
-
Как работают ограничения на сегменты?
{id:[0-9]+}— сегмент должен соответствовать регулярному выражению; иначе возвращается 404. -
Можно ли использовать
{:id}?
Нет, только{name}или{name:regexp}. -
Как быть с множеством методов для одного пути?
Зарегистрировать несколько обработчиков с разными методами, как в примере выше.
Как обрабатывать CORS?
Краткий ответ (30–60 секунд)
CORS (Cross-Origin Resource Sharing) управляется HTTP-заголовками. В Go можно использовать middleware, который добавляет Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers для ответов и обрабатывает preflight (OPTIONS). Либо пакет github.com/rs/cors.
Развёрнутый ответ
Простая реализация:
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
rs/cors предоставляет готовый cors.Cors и удобнее: настраивается разрешёнными origins, заголовками, разрешает preflight с кэшированием (Access-Control-Max-Age).
Под капотом (internals)
Middleware перехватывает запрос до основного обработчика. При preflight (OPTIONS с Origin и Access-Control-Request-Method) не доходит до основного кода. Браузеры отправляют preflight для "сложных" запросов (не simple). В Go нужно ответить 204 и необходимыми заголовками. Некоторые фреймворки (chi, gin) имеют встроенные обработчики.
Типичные ошибки кандидатов
-
Использование
Access-Control-Allow-Origin: *с credentials
ЕслиwithCredentials: true, нельзя использовать*. Нужно явно указать origin иAccess-Control-Allow-Credentials: true. -
Не обработка preflight запросов, которые приходят до основного
Без ответа браузер блокирует основной запрос. Middleware должен или возвращать 204, или передавать дальше, где обработчик может вернуть неверный статус. -
Путаница с заголовками на стороне клиента
Клиент отправляетAccess-Control-Request-Headers; сервер должен их разрешить и отразить вAccess-Control-Allow-Headers. Иногда забывают разрешитьAuthorization. -
Игнорирование
Varyзаголовка
Прокси-серверы могут кэшировать ответ; чтобы корректно кэшировать CORS ответы, нуженVary: Origin.
Возможные дополнительные вопросы интервьюера
-
Что такое simple request?
GET, HEAD, POST с определёнными заголовками (Content-Type только text/plain, multipart/form-data, application/x-www-form-urlencoded) — не вызывают preflight. -
Как разрешить несколько origins?
Нельзя перечислить через запятую, можно динамически проверятьOriginи копировать его вAllow-Origin. -
Как работает
Access-Control-Max-Age?
Указывает, сколько секунд браузер может кэшировать preflight-ответ.
Как работает http.Client с прокси?
Краткий ответ (30–60 секунд)
http.Client использует Transport.Proxy функцию для определения URL прокси. По умолчанию ProxyFromEnvironment читает переменные HTTP_PROXY, HTTPS_PROXY, NO_PROXY. Запросы к HTTPS отправляются методом CONNECT, создавая TCP-туннель. Можно задать свой прокси URL через Proxy: http.ProxyURL(...).
Развёрнутый ответ
Настройка явного прокси:
transport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
client := &http.Client{Transport: transport}
Для HTTPS транспорт устанавливает TCP-соединение с прокси, отправляет CONNECT host:port HTTP/1.1, прокси устанавливает туннель, после чего транспорт запускает TLS handshake итп. Поддерживаются и HTTP-прокси (для HTTP-запросов), которые просто перенаправляют запрос. Аутентификация поддерживается через заголовок Proxy-Authorization.
Также можно использовать ProxyConnectHeader для передачи специфичных заголовков в CONNECT.
Под капотом (internals)
Функция Proxy вызывается при каждом dial. Если возвращает не nil, транспорт не устанавливает обычное TCP-соединение, а вызывает proxyDial (конкретно для схемы). connectMethod управляет логикой. ProxyConnectHeader позволяет добавить заголовки, например для basic auth. При keep-alive прокси-соединения тоже кэшируются в пуле.
Типичные ошибки кандидатов
-
Забывают про
NO_PROXY
Когда программа запускается в окружении с глобальными переменными прокси, внезапные запросы к localhost идут через прокси. Нужно проверить и добавить исключения. -
Неправильная обработка HTTPS через прокси с самоподписными сертификатами
Прокси может разрывать TLS и подменять сертификат. По умолчанию Go проверяет сертификат конечного сервера. Если нет доверия, ошибка. -
Игнорирование
ProxyConnectHeaderдля авторизации прокси
Если прокси требует авторизации, нужно установитьProxy-Authorizationзаголовок вProxyConnectHeaderили в URL. -
Использование
Proxy: nilбез ProxyFromEnvironment
Явно задавnil, отключается использование переменных окружения, что обычно нежелательно для гибкости.
Возможные дополнительные вопросы интервьюера
-
Как задать разные прокси для разных хостов?
В функцииProxyанализировать URL запроса и возвращать соответствующий адрес прокси. -
Что произойдёт, если прокси недоступен?
Ошибка при CONNECT или dial;RoundTripвернёт ошибку. -
Поддерживается ли SOCKS прокси?
Да, черезgolang.org/x/net/proxy; создаётся dialer, который передаётся вTransport.DialContext.
Что такое httptrace и как его использовать для отладки?
Краткий ответ (30–60 секунд)
httptrace — пакет для трассировки HTTP-запросов: захват событий (DNS, TCP, TLS, got conn, wrote request, got first byte). Позволяет диагностировать задержки и ошибки. Используется через httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{...}).
Развёрнутый ответ
Пример:
trace := &httptrace.ClientTrace{
DNSDone: func(info httptrace.DNSDoneInfo) {
fmt.Printf("DNS resolved: %v\n", info.Addrs)
},
GotConn: func(info httptrace.GotConnInfo) {
fmt.Printf("Connection reused: %v\n", info.Reused)
},
TLSHandshakeDone: func(state tls.ConnectionState, err error) {
fmt.Printf("TLS handshake done\n")
},
GotFirstResponseByte: func() {
fmt.Printf("First byte received\n")
},
}
ctx := httptrace.WithClientTrace(context.Background(), trace)
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
http.DefaultClient.Do(req)
Выводит временные метки, позволяя профилировать медленные этапы. В production можно записывать в лог или метрики.
Под капотом (internals)
ClientTrace содержит множество колбэков, которые вызываются транспортом и net пакетом в ключевых точках: GetConn (перед открытием), DNSStart/DNSDone, ConnectStart/ConnectDone, TLSHandshakeStart/Done, WroteRequest, GotFirstResponseByte. Трассировка реализована через контекст: httptrace.ContextClientTrace(ctx) извлекает зарегистрированный трейс. Производительность: если колбэк nil, оверхед минимален.
Типичные ошибки кандидатов
-
Забывают передать контекст в запрос
Трассировка привязывается к контексту; безWithClientTraceи использования контекста вNewRequest, колбэки не вызовутся. -
Сохранение контекста с trace на длительный срок
Контекст содержит ссылки, что может удерживать память; после запроса контекст нужно отменять. -
Избыточное логирование в колбэках для каждого запроса
Запись в лог на каждый пакет может замедлить систему и создать шум. Использовать выборочно или с агрегацией. -
Неправильное использование
GotConnдля суждения о переиспользовании
Reusedпоказывает, что соединение взято из idle пула; бывают ситуации, когда соединение не reused из-за таймаутов, но это нормально.
Возможные дополнительные вопросы интервьюера
-
Как измерить время DNS-запроса?
Запомнить время вDNSStart, вычислить разницу вDNSDone. -
Как с помощью трассировки найти утечку соединений?
СчитатьPutIdleConnиGotConn— соотношение покажет, возвращаются ли соединения в пул. -
Можно ли использовать
httptraceна серверной стороне?
Нет, только для клиента. На сервере можно использоватьnet/http/httptestи middleware для логирования.
Как работает graceful shutdown для HTTP сервера?
Краткий ответ (30–60 секунд)
Graceful shutdown позволяет завершить HTTP-сервер без обрыва активных запросов: server.Shutdown(ctx) закрывает листенер, не принимает новые соединения, ждёт завершения текущих запросов или истечения контекста. После этого сервер завершается.
Развёрнутый ответ
Паттерн:
srv := &http.Server{Addr: ":8080"}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Server forced to shutdown: %v", err)
}
Shutdown вызывает Close на листенере, затем ждёт, пока все активные соединения завершат обработку (горутины обслуживания). Если контекст истекает, соединения обрываются. Важно, чтобы обработчики учитывали контекст запроса и завершались при отмене.
Под капотом (internals)
Shutdown устанавливает состояние сервера в shuttingDown. В цикле Accept после закрытия листенера возвращается http.ErrServerClosed. Для каждого активного соединения Serve горутина ожидает завершения. Реализован механизм отслеживания через server.wg (WaitGroup) для горутин обслуживания и пула соединений. При истечении контекста оставшиеся соединения закрываются принудительно.
Типичные ошибки кандидатов
-
Игнорирование ошибки
http.ErrServerClosedприListenAndServe
ПослеShutdownListenAndServeвозвращаетhttp.ErrServerClosed; если её не обработать, программа запаникует или некорректно завершится. -
Не передача контекста с таймаутом в
Shutdown
Shutdown(nil)илиcontext.Background()может висеть бесконечно, если обработчики не завершаются. Нужен timeout. -
Блокирующие обработчики, игнорирующие
r.Context().Done()
ТогдаShutdownне сможет корректно завершить их, пока не истечёт таймаут, что вызывает принудительное закрытие. -
Запуск нескольких вызовов
Shutdown
Повторный вызов вызовет панику; нужно синхронизировать.
Возможные дополнительные вопросы интервьюера
-
Как корректно завершить базу данных после сервера?
ПослеShutdownзакрыть соединения БД. -
Что такое
RegisterOnShutdown?
Методhttp.Server, позволяющий зарегистрировать функции, выполняемые приShutdown. -
Можно ли использовать
CloseвместоShutdown?
Closeнемедленно обрывает все соединения, что нежелательно.
Как работает SO_REUSEPORT в Go?
Краткий ответ (30–60 секунд)
SO_REUSEPORT — опция сокета, позволяющая нескольким процессам или потокам одновременно слушать один и тот же адрес и порт. В Linux ядро распределяет входящие соединения между ними. В Go включается через net.ListenConfig.Control, где вызывается syscall.SetsockoptInt.
Развёрнутый ответ
Пример:
var lc net.ListenConfig
lc.Control = func(network, address string, c syscall.RawConn) error {
var operr error
err := c.Control(func(fd uintptr) {
operr = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEPORT, 1)
})
if err != nil { return err }
return operr
}
ln, _ := lc.Listen(context.Background(), "tcp", ":8080")
После этого несколько экземпляров одного приложения могут разделять нагрузку. Kernel распределяет соединения по модели round-robin на основе хеша (пять кортежей). Это улучшает масштабирование и позволяет бесшовный перезапуск без потери порта.
Под капотом (internals)
В Linux SO_REUSEPORT введён в ядре 3.9, использует reuseport механизм. Когда сокет помечен, ядро позволяет нескольким сокетам bind один адрес. При поступлении SYN ядро выбирает один из слушающих сокетов (по хешу). Это отличается от SO_REUSEADDR, который только разрешает bind того же адреса после TIME_WAIT. В Go net.ListenConfig позволяет задать Control до bind. На уровне net/http для сервера можно использовать Server.ListenAndServe с предварительно созданным листенером.
Типичные ошибки кандидатов
-
Путаница с
SO_REUSEADDR
SO_REUSEADDRпозволяет переиспользовать адрес в TIME_WAIT, но не разрешает нескольким слушать одновременно.SO_REUSEPORTрешает другую задачу. -
Ожидание round-robin без учёта хеширования
Ядро распределяет соединения детерминированно по хешу, что может привести к неравномерности; не всегда round-robin. -
Использование без поддержки ОС
На старых ядрах или Windows может отсутствовать; нужна проверка. -
Некорректный graceful restart
При использовании SO_REUSEPORT новый процесс может начать принимать запросы до готовности; нужно синхронизировать.
Возможные дополнительные вопросы интервьюера
-
Чем SO_REUSEPORT помогает в graceful restart?
Можно запустить новый процесс на том же порту, пока старый завершается, без потери соединений (при использовании listener transfer или через SO_REUSEPORT). -
Как это работает с epoll/kqueue?
Ядро добавляет соединение в очередь приёма только одного сокета, поэтому epoll на разных файловых дескрипторах получает непересекающиеся события. -
Какие альтернативы для балансировки?
Внешний reverse proxy (nginx, haproxy) илиSO_REUSEPORT.
Как работает fasthttp и чем он отличается от net/http?
Краткий ответ (30–60 секунд)
fasthttp — высокопроизводительная HTTP-библиотека, оптимизированная для минимального выделения памяти и аллокаций. Использует пулы объектов (буферов, request/response), избегает создания горутин на каждое соединение, позволяет zero-copy парсинг. net/http предоставляет более простую модель, но дороже по памяти и CPU.
Развёрнутый ответ
Отличия fasthttp:
- Пул буферов:
Request,Responseпереиспользуются, что снижает GC. - Worker pool: вместо
go handle(conn)используется фиксированный пул воркеров, уменьшая число горутин. - Собственный парсер: высокооптимизированный, без выделения строк для заголовков (байтовые слайсы).
- Несовместимость:
fasthttp.Requestне реализуетhttp.Request, поэтому не подходит для стандартных middleware и gRPC (требуется адаптер). - Производительность: на бенчмарках выигрывает в разы, но плата — сложность и потенциальные проблемы с совместимостью.
Когда использовать: высоконагруженные роутеры, прокси, где задержка и память критичны. Для большинства приложений достаточно net/http.
Под капотом (internals)
fasthttp.Server имеет собственную горутину-акцептор, которая передаёт сырой net.Conn в пул воркеров. Каждый воркер обрабатывает несколько соединений по эвентовой модели, используя неблокирующее ожидание через epoll (кастомный), не создавая горутину на каждое соединение. Буферы для парсинга и записи берутся из sync.Pool. Отсутствует стандартная поддержка HTTP/2 (требуется внешний пакет fasthttp/http2). Заголовки представлены как []byte, а не строки.
Типичные ошибки кандидатов
-
Бежать за
fasthttpсразу
Преждевременная оптимизация. Убедитесь, что стандартныйnet/httpузкое место;fasthttpусложняет код, отладку, интеграцию (middleware, трассировка, OpenTelemetry). -
Утечка памяти из-за неправильного управления пулом
Объекты, взятые из пула, должны возвращаться; если передавать их в горутины без возврата, пул опустошается. -
Неверное использование
RequestCtx
Контекстfasthttp.RequestCtxживет только во время обработки; сохранение ссылок на него вне хендлера ведёт к гонкам и use-after-free. -
Игнорирование таймаутов
Параметрыfasthttp.Server(ReadTimeout,WriteTimeout) работают иначе; нужно тщательно тестировать.
Возможные дополнительные вопросы интервьюера
-
Как
fasthttpработает с TLS?
Аналогично, но без автоматического HTTP/2; использует структуры изcrypto/tls. -
Можно ли совмещать
fasthttpсnet/http?
Есть адаптерыfasthttpadaptorдля оборачиванияhttp.Handlerвfasthttp.RequestHandlerи наоборот, но с потерей производительности. -
Какие ограничения у
fasthttp?
Нет поддержки HTTP/2 из коробки, ограниченная поддержка стриминга, несовместимость с библиотеками, ожидающимиhttp.ResponseWriter.
Как обрабатывать multipart/form-data?
Краткий ответ (30–60 секунд)
Использовать r.ParseMultipartForm(maxMemory), затем r.MultipartForm.File и r.MultipartForm.Value. Для потоковой обработки больших файлов — r.MultipartReader(), который итеративно возвращает *multipart.Part, позволяя читать тело файла как io.Reader.
Развёрнутый ответ
Потоковый подход:
reader, _ := r.MultipartReader()
for {
part, _ := reader.NextPart()
if part == nil { break }
if part.FormName() == "file" {
// читать part
io.Copy(file, part)
}
part.Close()
}
maxMemory определяет, сколько данных кэшируется в памяти; остальное пишется во временные файлы. Для загрузки больших файлов лучше использовать потоковый интерфейс, чтобы не заполнять память. После закрытия частей временные файлы удаляются.
Под капотом (internals)
r.MultipartReader() парсит границу из Content-Type. Возвращает mime/multipart.Reader, который читает r.Body с учётом буфера. При достижении границы создаётся Part, содержащий заголовки части и тело (обёрнутое в io.LimitedReader для ограничения размера, если задан maxMemory). При ParseMultipartForm все файлы и поля сохраняются в памяти или временных файлах в os.TempDir.
Типичные ошибки кандидатов
-
Передача гигантского
maxMemory(например 1GB)
Это может забить всю ОЗУ; лучше держать низкое значение (например 32MB) и обрабатывать потоково. -
Неправильная обработка ошибок NextPart при обрыве соединения
NextPartможет вернуть ошибку разбора; тогда нужно прекратить чтение и вернуть ошибку клиенту. -
Не настроен лимит размера файла
MultipartReaderне ограничивает размер одной части; нужно обернутьpartвio.LimitReaderс разумным пределом. -
Игнорирование закрытия
part
Part.Close()освобождает ресурсы; если не закрывать, могут утекать временные файлы.
Возможные дополнительные вопросы интервьюера
-
Как отправить multipart запрос из Go клиента?
Использоватьmime/multipart.Writerдля создания тела,http.Postс Content-Type. -
Что делать, если нужно принять несколько файлов в одном поле?
Проитерироваться поMultipartForm.File["field"]— это слайс*FileHeader. -
Как обрабатывать большие JSON-тела вместе с файлами?
Multipart позволяет смешивать поля и файлы; значения читаются изValuemap.
Как работает Server-Sent Events (SSE)?
Краткий ответ (30–60 секунд)
SSE — односторонний поток от сервера к клиенту поверх HTTP. Сервер устанавливает Content-Type: text/event-stream, использует chunked Transfer-Encoding, отправляет строки вида data: ...\n\n. Клиент использует EventSource API в браузере или в Go — чтение из resp.Body с распознаванием событий.
Развёрнутый ответ
Сервер:
func sseHandler(w http.ResponseWriter, r *http.Request) {
flusher, ok := w.(http.Flusher)
if !ok { ... }
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
for {
msg := "data: " + time.Now().String() + "\n\n"
fmt.Fprint(w, msg)
flusher.Flush()
time.Sleep(1 * time.Second)
// check context
}
}
Клиент в Go:
resp, _ := client.Do(req)
defer resp.Body.Close()
reader := bufio.NewReader(resp.Body)
for {
line, _ := reader.ReadString('\n')
if strings.HasPrefix(line, "data:") {
// process
}
}
SSE прост в реализации, поддерживает авто-реконнект (браузер), идентификаторы событий (id), типы (event). Заголовок Last-Event-ID для возобновления.
Под капотом (internals)
На сервере ResponseWriter с Flusher гарантирует немедленную отправку данных. Chunked encoding позволяет отправлять куски без финального размера. На уровне TCP это обычный текст. Браузеры автоматически переподключаются при разрыве. В Go можно использовать gorilla/mux или chi для маршрутизации. Есть библиотеки, например r3labs/sse, упрощающие отправку событий.
Типичные ошибки кандидатов
-
Не отправка первичного комментария для поддержки старых прокси
Некоторые прокси буферизуют данные; отправка:ok\n\nпомогает инициировать соединение. -
Забывают установить
Status OKпередFlush
Иначе браузер может считать ошибкой.WriteHeader(200)— неплохо явно задать. -
Блокирование цикла отправки без ухода при отключении клиента
Нужно слушатьr.Context().Done(), чтобы выйти и освободить горутину и соединение. -
Отсутствие keep-alive heartbeat
Если данных долго нет, промежуточные узлы могут закрыть соединение. Нужно отправлять комментарии: heartbeatпериодически.
Возможные дополнительные вопросы интервьюера
-
Как обрабатывать переподключение с последнего события?
Клиент шлёт заголовокLast-Event-ID; сервер может хранить события и переотправлять. -
Можно ли использовать HTTP/2 для SSE?
Да, но браузеры не всегда разрешают; обычно через HTTP/1.1 long-lived соединение. -
Чем SSE отличается от WebSocket?
SSE — однонаправленный, проще; WebSocket — полнодуплексный, бинарный.
Как настроить keep-alive для клиента и сервера?
Краткий ответ (30–60 секунд)
Сервер: http.Server.IdleTimeout — время ожидания следующего запроса по keep-alive. Клиент: http.Transport поддерживает keep-alive по умолчанию, настраивается IdleConnTimeout (время простоя в пуле), MaxIdleConns, MaxIdleConnsPerHost. Также нужно читать тело ответа до конца, чтобы соединение возвращалось в пул.
Развёрнутый ответ
Конфигурация сервера:
srv := &http.Server{
Addr: ":8080",
IdleTimeout: 90 * time.Second, // keep-alive timeout
}
Клиент:
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 30 * time.Second, // меньше серверного
}
client := &http.Client{Transport: transport}
Для TCP keep-alive используют net.Dialer.KeepAlive (для обнаружения мёртвых соединений). Это отдельная опция, которая передаётся через Transport.DialContext.
transport.DialContext = (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second, // TCP keep-alive
}).DialContext
Важно: HTTP keep-alive (переиспользование TCP) и TCP keep-alive — разные вещи. Обе дополняют друг друга.
Под капотом (internals)
Серверный IdleTimeout реализован через таймер, который сбрасывается при прочтении нового запроса. Если истекает, сервер закрывает соединение. Клиентский IdleConnTimeout контролирует нахождение persistConn в пуле; при изъятии таймер останавливается. DisableKeepAlives = true отключает HTTP keep-alive для клиента.
Типичные ошибки кандидатов
-
Путаница между серверным и клиентским таймаутами
Иногда задают оба одинаковыми, но клиентский должен быть чуть меньше серверного, чтобы избежать использования закрытых сервером соединений. -
Не настроен TCP keep-alive, полагаются только на HTTP
Если HTTP keep-alive держит соединение открытым, но по сети нет трафика долго, NAT/фаерволы закроют; TCP keep-alive решает эту проблему. -
Не учитывают, что HTTP/2 имеет свои keep-alive пинги
Для HTTP/2 keep-alive через PING фреймы настраиваются черезhttp2.Transport(поле PingTimeout, PingResponse). -
Отключение keep-alive без понимания последствий
Каждый новый запрос создаёт новое TCP-соединение, увеличивая latency и потребление ресурсов.
Возможные дополнительные вопросы интервьюера
-
Как проверить, что keep-alive работает?
Использоватьnetstatдля просмотра состояний TCP, илиhttptraceколбэкиGotConn.Reused. -
Какие заголовки управляют keep-alive?
Connection: keep-alive(устарел),Keep-Alive: timeout=5, max=100— но в HTTP/1.1 игнорируются, сервер сам решает. -
Можно ли отключить keep-alive на уровне отдельного ответа?
Да,w.Header().Set("Connection", "close").
Как работает net.LookupHost для разрешения DNS?
Краткий ответ (30–60 секунд)
net.LookupHost преобразует доменное имя в список IP-адресов, используя системный резолвер (C библиотека или встроенный Go-резолвер). В Go по умолчанию используется встроенный резолвер, который делает параллельные запросы к настроенным DNS-серверам (из /etc/resolv.conf). Возвращает слайс строк IP.
Развёрнутый ответ
Пример:
ips, err := net.LookupHost("example.com")
По умолчанию Go резолвер использует нативный Go DNS-клиент (net.Resolver), который считывает настройки из resolv.conf. Он выполняет запросы A и AAAA параллельно, реализует happy eyeballs. Можно кастомизировать через Resolver.Dial или использовать системный (CGO) через env GODEBUG=netdns=cgo.
Для низкоуровневого контроля можно использовать net.Resolver.LookupIPAddr(ctx, host).
Под капотом (internals)
Встроенный резолвер в пакете net реализован в dnsclient_unix.go и dnsclient.go. Он отправляет UDP-запросы (и TCP при необходимости) к серверам из resolv.conf. Обрабатывает таймауты, повторы. Кэширование DNS происходит на уровне ОС, не в Go (если не используется CGO). В Go 1.22+ улучшена обработка ошибок и таймаутов. При отмене контекста запрос прерывается.
Типичные ошибки кандидатов
-
Игнорирование контекста
LookupHostбез контекста может зависнуть на неопределённое время; нужно использоватьnet.DefaultResolver.LookupHost(ctx, host)с таймаутом. -
Предположение, что результат всегда содержит один IP
Возвращается слайс; клиентский код должен обрабатывать все адреса или использоватьDialer.Resolverдля проб. -
Не проверка ошибки на временный сбой DNS
DNS может вернуть временную ошибку; стоит иметь retry или fallback. -
Использование
net.LookupHostдля проверки доступности хоста
Это некорректно; лучше делатьnet.Dial.
Возможные дополнительные вопросы интервьюера
-
Как настроить кастомный DNS-сервер для резолвера?
Использоватьnet.Resolver{PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { return dnsDialer.DialContext(ctx, network, "8.8.8.8:53") }}. -
Чем отличается
LookupIPAddrотLookupHost?
LookupIPAddrвозвращает[]net.IPAddr, предоставляя больше информации (зона),LookupHost— строки. -
Как работает кэширование DNS?
В Go нет встроенного кэша, но ОС кэширует, что может приводить к задержкам обновления. Для кастомного кэширования используют библиотеки.
Как работает net/http/httputil.ReverseProxy?
Краткий ответ (30–60 секунд)
ReverseProxy — стандартный реверс-прокси в Go. Он принимает входящие запросы, изменяет URL, копирует заголовки, выполняет запрос к бэкенду, копирует ответ обратно клиенту. Поддерживает WebSocket (через Hijack), модификацию запроса/ответа, балансировку.
Развёрнутый ответ
Пример базового прокси:
proxy := httputil.NewSingleHostReverseProxy(targetURL)
log.Fatal(http.ListenAndServe(":8888", proxy))
Можно переопределять Director для модификации запроса, ModifyResponse для изменения ответа, ErrorHandler. Для WebSocket проксирование поддерживается автоматически при наличии заголовка Upgrade.
Также можно реализовать балансировку между несколькими бэкендами, изменяя Director.
Под капотом (internals)
ReverseProxy.ServeHTTP:
- Клонирует входящий запрос, создаёт outbound запрос.
- Вызывает
Director(req)для настройки outbound запроса (URL, заголовки). - Выполняет
transport.RoundTrip(outReq). - Если ответ получен, копирует заголовки ответа в
w, затемio.Copy(w, resp.Body). - Обрабатывает Upgrade (Hijack): для WebSocket захватывает нижележащее соединение и тунельирует.
Поддерживает FlushInterval для стриминга. BufferPool для управления буферами копирования.
Типичные ошибки кандидатов
-
Не корректная обработка
X-Forwarded-For
Прокси должен добавлять IP клиента вX-Forwarded-For, иначе бэкенд не имеет информации.NewSingleHostReverseProxyне делает этого автоматически; нужно вDirectorили использоватьhttputil.ReverseProxyи настраивать. -
Забывают про
Hostзаголовок
Directorдолжен установитьoutReq.Host = target.Host, иначе бэкенд может получить неправильный Host. -
Копирование тела без учета Content-Length/Chunked
Тело копируется черезio.Copy, что нормально, но если ответ сContent-Encoding: gzip, может возникнуть двойное сжатие, если не правильно настроенModifyResponse. Также грейсфул завершение (ожидание конца копирования) важно. -
Не настроен таймаут на транспорт
По умолчаниюReverseProxyиспользуетhttp.DefaultTransport. Лучше задать кастомный транспорт с таймаутами и ограничениями.
Возможные дополнительные вопросы интервьюера
-
Как реализовать circuit breaker?
ВTransportобернутьRoundTripperили использовать библиотеку; при ошибках возвращать fallback ответ. -
Как проксировать WebSocket?
ReverseProxyавтоматически обрабатывает Upgrade, но нужно убедиться, чтоDirectorне удаляет заголовкиConnectionиUpgrade. -
Как изменить путь при проксировании?
ВDirectorмодифицироватьoutReq.URL.Path.
Что такое DrainBody и почему важно вычитывать тело ответа до конца?
Краткий ответ (30–60 секунд)
DrainBody — неофициальный термин для полного чтения и закрытия тела HTTP-ответа. При использовании keep-alive, если не прочитать тело до конца и не закрыть его, TCP-соединение не может быть переиспользовано. Это приводит к утечке соединений и истощению ресурсов.
Развёрнутый ответ
В Go для клиента: resp.Body.Close() просто закрывает reader, но если остались непрочитанные данные, соединение не вернётся в пул. Нужно гарантированно прочитать остаток и закрыть. Лучшая практика:
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
Это особенно важно при редиректах, когда http.Client не вычитывает тела у промежуточных ответов. Или можно использовать defer resp.Body.Close() после чтения, но перед этим обязательно io.Copy(io.Discard, ...).
Под капотом (internals)
В Transport.persistConn возвращение в idleConn происходит при закрытии тела, если не было ошибок и нет незавершённого чтения. Если Body.Close вызван, но в bufio.Reader остались данные, соединение не считается чистым и не кешируется. Это предотвращает случайное чтение следующего ответа с мусором.
Типичные ошибки кандидатов
-
Закрытие тела без вычитки
Думают, чтоresp.Body.Close()достаточно. Многие делаютdefer resp.Body.Close(), не читая тело, и удивляются, что соединения не переиспользуются. -
Паника при закрытии тела, если ответ nil
Приerr != nilresp может быть nil. Проверятьif resp != nil && resp.Body != nil. -
Игнорирование в случае редиректов
http.Clientпри редиректах может не вычитывать тело; customCheckRedirectможет потребоваться. -
Использование
io.LimitedReaderбез вычитки остатка
Если ограничили чтение, всё равно нужно вычитать остаток для keep-alive.
Возможные дополнительные вопросы интервьюера
-
Как принудительно закрыть соединение без вычитки?
Установитьreq.Close = trueилиConnection: close. -
Как проверить, что соединение вернулось в пул?
Использоватьhttptrace.PutIdleConn. -
Отличается ли поведение для HTTP/2?
В HTTP/2 закрытие стрима выполняется после полного прочтения тела, но само TCP-соединение не закрывается; не вычитанное тело может привести к утечке памяти в потоке.
Как работает http.RoundTripper и как его можно мокать?
Краткий ответ (30–60 секунд)
http.RoundTripper — интерфейс, определяющий RoundTrip(*Request) (*Response, error). Это ядро клиентского транспорта. Мокать можно, создав свою реализацию, возвращающую предопределённый ответ, или обернув существующий транспорт для перехвата запросов (для тестов, логирования, добавления заголовков).
Развёрнутый ответ
Интерфейс:
type RoundTripper interface {
RoundTrip(*Request) (*Response, error)
}
http.Client вызывает его. По умолчанию используется http.DefaultTransport. Мок для тестов:
type mockTransport struct {
response *http.Response
}
func (m *mockTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return m.response, nil
}
client := &http.Client{Transport: &mockTransport{resp}}
Можно также обернуть существующий транспорт для логирования, добавления заголовков, метрик.
Под капотом (internals)
В стандартном Transport RoundTrip сложный: обрабатывает схему URL, прокси, выбирает или создаёт persistConn, записывает запрос, читает ответ, обрабатывает редиректы (но редиректы в клиенте выше). RoundTripper позволяет цепочку: несколько обёрток, где каждая вызывает next.RoundTrip.
Типичные ошибки кандидатов
-
Некорректное поведение мока: не закрывают тело ответа
Мока ответ должен содержатьBody, которое при закрытии ничего не делает, но тесты могут падать, если тело nil. -
Модификация
Requestпосле передачи вRoundTrip
Нельзя изменять запрос после вызова, это нарушает контракт. -
Забывают про контекст и его отмену
Если мока долгая, должна реагировать наreq.Context().Done(), чтобы не висеть. -
Использование мока как замена функционального теста
Полностью мокая транспорт, теряется интеграционное покрытие; комбинировать с интеграционными тестами.
Возможные дополнительные вопросы интервьюера
-
Как добавить OAuth2 токен ко всем запросам?
Создать кастомныйRoundTripper, оборачивающий транспорт и добавляющий заголовок вRoundTrip. -
Как написать
RoundTripper, который кэширует ответы?
Проверять метод, кэш-ключ, при попадании возвращать сохранённый ответ. -
Чем отличается
RoundTripperотHandler?
RoundTripperдля клиента, делает запрос и получает ответ.Handlerдля сервера, принимает запрос и формирует ответ.
Как работать с x-www-form-urlencoded данными?
Краткий ответ (30–60 секунд)
Это format запроса с Content-Type: application/x-www-form-urlencoded, где данные кодируются как key=value&... с URL-кодированием. В Go для парсинга используется r.ParseForm(), значения доступны через r.Form. Для клиента — url.Values с методом Encode().
Развёрнутый ответ
На сервере:
func handler(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
name := r.Form.Get("name")
// или r.PostForm для POST
}
Клиент:
form := url.Values{}
form.Set("name", "Alice")
form.Set("age", "30")
body := form.Encode()
resp, _ := http.Post("http://example.com", "application/x-www-form-urlencoded", strings.NewReader(body))
ParseForm также обрабатывает query-параметры. Важно: для загрузки больших данных не рекомендуется из-за низкой эффективности бинарных данных.
Под капотом (internals)
ParseForm вызывает r.Body чтение и парсит URL-кодированные данные в url.Values. Используется url.ParseQuery. Максимальный размер тела для разбора задаётся MaxFormSize (по умолчанию 10MB). r.Form включает и URL-параметры, и тело для POST/PUT. При конфликте параметр из query идёт первым.
Типичные ошибки кандидатов
-
Забывают вызвать
ParseFormперед доступом кr.Form
Иначеr.Formпуст.ParseFormвызывается обычно в middleware. -
Перепутать
r.Formиr.PostForm
r.Formсодержит и URL-параметры,r.PostFormтолько тело POST/PUT. -
Отправка бинарных данных как form-urlencoded
Неэффективно, предназначено только для текстовых данных. Для файлов — multipart. -
Не проверка ошибок
ParseForm
Может вернуть ошибку при превышении размера или неправильном формате; нужно обрабатывать.
Возможные дополнительные вопросы интервьюера
-
Как закодировать вложенные структуры в form-urlencoded?
Обычно не поддерживается; можно имитировать через квадратные скобкиkey[sub]=value, но нужно кастомное кодирование. -
Как отправить массивы значений?
key=val1&key=val2—url.Values.Add("key", "val1"); Add("key", "val2"). -
В чём отличие от multipart/form-data?
multipart для загрузки файлов, form-urlencoded для простых текстовых форм.
Как работает h2c (HTTP/2 без TLS)?
Краткий ответ (30–60 секунд)
h2c — HTTP/2 поверх незашифрованного TCP. Клиент и сервер договариваются через механизм Upgrade (HTTP/1.1 Upgrade: h2c). В Go используется x/net/http2/h2c или golang.org/x/net/http2 с ручной настройкой.
Развёрнутый ответ
Сервер:
import "golang.org/x/net/http2"
import "golang.org/x/net/http2/h2c"
s := &http.Server{Addr: ":8080", Handler: mux}
http2.ConfigureServer(s, &http2.Server{})
log.Fatal(s.ListenAndServe())
// Обёртка с h2c
http.ListenAndServe(":8080", h2c.NewHandler(mux, &http2.Server{}))
Клиент:
client := &http.Client{
Transport: &http2.Transport{
AllowHTTP: true,
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
return net.Dial(network, addr)
},
},
}
Или использовать h2c.NewClient. Используется для внутренних сервисов, где не нужен TLS overhead.
Под капотом (internals)
Без TLS, h2c.NewHandler оборачивает стандартный HTTP/1.1 сервер и перехватывает запросы с Upgrade: h2c, HTTP2-Settings. При успешном обновлении соединение передаётся в http2.Server. Иначе продолжается HTTP/1.1. На транспортном уровне http2.Transport с AllowHTTP: true и DialTLS, который просто создаёт TCP-соединение, инициирует Upgrade. Flow control и другие механизмы работают аналогично TLS версии.
Типичные ошибки кандидатов
-
Думать, что h2c работает "из коробки" без настройки
Стандартныйnet/httpне поддерживает h2c; нужен явный вызовh2c.NewHandlerилиhttp2.TransportсAllowHTTP. -
Использовать h2c для внешних клиентов
Многие сети блокируют не TLS трафик; для публичного API используйте HTTPS. -
Забывают настроить
AllowHTTPи кастомныйDialTLS
Иначе транспорт пытается установить TLS соединение, и запрос падает. -
Неправильный порядок обновления
Если сервер не возвращает 101 Switching Protocols, клиент остаётся на HTTP/1.1; важно логировать для отладки.
Возможные дополнительные вопросы интервьюера
-
Зачем использовать h2c?
Для backend-коммуникаций, когда TLS избыточен и нужна производительность; мультиплексирование. -
Как включить h2c на gRPC сервере?
grpc.NewServerсgrpc.Insecure()и использование h2c транспорта. -
Какие ограничения у h2c?
Отсутствие шифрования, невозможность использования в браузере (браузеры не поддерживают), возможны проблемы с прокси.
Как реализовать "long polling" с помощью Go?
Краткий ответ (30–60 секунд)
Long polling — клиент отправляет HTTP-запрос, сервер удерживает соединение открытым до появления данных или таймаута, затем отвечает. Клиент сразу же отправляет новый запрос. В Go сервер просто блокирует обработчик (ожидая через канал или контекст) и пишет ответ, когда готов. Использовать таймауты, чтобы не зависать навечно.
Развёрнутый ответ
Серверная реализация:
func longPollHandler(w http.ResponseWriter, r *http.Request) {
// ждем данные не дольше 30 сек
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second)
defer cancel()
select {
case msg := <-messageChannel:
fmt.Fprint(w, msg)
case <-ctx.Done():
http.Error(w, "timeout", 304)
}
}
Клиент в цикле делает запросы; при получении ответа или таймаута — снова запрашивает.
Под капотом (internals)
Сервер блокирует горутину на чтение канала или контекста. Пока соединение открыто, TCP keep-alive помогает не разрываться промежуточными устройствами. Нужно настроить WriteTimeout сервера достаточно большим, чтобы покрыть максимальный интервал опроса. Так как горутина дешева, можно обслуживать тысячи long-polling соединений.
Типичные ошибки кандидатов
-
Отсутствие таймаута ожидания
Соединение может висеть бесконечно, занимая сокет и горутину. Нужно обязательно ограничивать время ожидания. -
Блокировка горутины без учета закрытия соединения клиентом
Использоватьr.Context().Done()илиw.(http.CloseNotifier).CloseNotify()(устарело) для выхода при обрыве. -
Неправильный выбор между long polling и WebSocket/SSE
Использовать long polling для редких событий, где WebSocket избыточен; но для частых обновлений он создаёт лишние запросы. -
Не установлены таймауты на стороне клиента
Клиент должен иметь timeout больше серверного, чтобы обработать ошибку и переподключиться.
Возможные дополнительные вопросы интервьюера
-
Как обрабатывать несколько клиентов, ждущих одно событие?
Использовать брокер (канал, pub/sub), который одновременно уведомляет всех подписчиков. -
Как отличить long polling от обычного polling?
Обычный — клиент делает запросы с интервалом, long — сервер держит до появления данных. -
Как реализовать long polling с балансировщиком нагрузки?
Балансировщик должен поддерживать длинные соединения и не разрывать их по idle таймауту.
Как тестировать HTTP хендлеры с помощью httptest?
Краткий ответ (30–60 секунд)
Пакет net/http/httptest предоставляет httptest.NewServer (для интеграционных тестов) и httptest.NewRecorder (для unit-тестов обработчиков). NewRecorder реализует http.ResponseWriter, позволяя проверить статус, заголовки и тело без реального сервера.
Развёрнутый ответ
Unit-test хендлера:
func TestHandler(t *testing.T) {
handler := http.HandlerFunc(myHandler)
req := httptest.NewRequest("GET", "/path?key=val", nil)
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
resp := w.Result()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 {
t.Errorf("expected 200, got %d", resp.StatusCode)
}
}
Интеграционный тест с реальным TCP:
ts := httptest.NewServer(handler)
defer ts.Close()
resp, err := http.Get(ts.URL + "/path")
httptest.Server автоматически выбирает свободный порт.
Под капотом (internals)
ResponseRecorder записывает статус, заголовки, тело в bytes.Buffer. Реализует http.ResponseWriter и дополнительные интерфейсы (Flusher, Push, Hijacker — заглушка). NewRequest создаёт http.Request с контекстом и возможностью установки заголовков. Сервер запускает реальный TCP-сервер на локалхосте.
Типичные ошибки кандидатов
-
Не читают тело ответа в recorder перед проверкой
Послеw.Result()тело доступно, но нужно закрыть, чтобы освободить ресурсы. -
Ошибочное использование
httptest.NewServerдля каждого теста (медленно)
Для юнит-тестов хендлеров достаточноNewRecorder;NewServerдля интеграционных. -
Забывают проверить заголовки после вызова
Write
В recorder можно проверять заголовки в любое время, но в реальном ответе послеWriteHeaderзаголовки фиксируются. -
Не тестируют крайние случаи: ошибки при записи
ResponseRecorderне эмулирует ошибкуWrite. Для этого нужны моки.
Возможные дополнительные вопросы интервьюера
-
Как тестировать middleware?
СоздатьNewRecorder, запрос, обернуть тестовый обработчик в middleware и выполнитьServeHTTP. -
Как проверить, что хендлер установил определенный кук?
resp.Result().Cookies()илиw.Header().Get("Set-Cookie"). -
Можно ли симулировать разрыв соединения?
ЧерезNewRecorderнет; нужно создавать кастомныйResponseWriter, который наWriteвозвращает ошибку, либо использоватьhttptest.Serverи закрывать клиента.
Как ограничить количество входящих соединений на HTTP сервере?
Краткий ответ (30–60 секунд)
Использовать семафор (буферизованный канал) в net.Listener обёртке, ограничивающей вызов Accept, либо в middleware, который держит счётчик активных запросов. Также можно настраивать системные лимиты (backlog, ulimit). В Go 1.22+ есть http.Server.MaxConnsPerIP, MaxConns. Но они не экспортированы; необходимо использовать кастомный листенер или netutil.
Развёрнутый ответ
Кастомный листенер с ограничением:
type limitListener struct {
net.Listener
sem chan struct{}
}
func (l *limitListener) Accept() (net.Conn, error) {
l.sem <- struct{}{}
return l.Listener.Accept()
}
func (l *limitListener) release() {
<-l.sem
}
// при использовании
ln, _ := net.Listen("tcp", ":8080")
limited := &limitListener{Listener: ln, sem: make(chan struct{}, maxConns)}
for {
conn, _ := limited.Accept()
go func() {
defer limited.release()
handle(conn)
}()
}
Другой способ: middleware, который с помощью счётчика (atomic) лимитирует запросы, возвращая 503 при превышении. Это не предотвращает открытие TCP-соединения, но ограничивает нагрузку на обработчики.
Под капотом (internals)
В http.Server внутренняя структура отслеживает количество активных соединений, но публичных методов ограничения нет до версии 1.22 (где есть MaxConns? На самом деле в Go 1.22 добавили http.Server.MaxConns и MaxConnsPerIP? Проверим: в экспериментах http.Server имеет поля MaxConns и MaxConnsPerIP в Go 1.22 — да, добавлены. Они позволяют ограничить количество соединений на уровне сервера. Использование: srv.MaxConns = 1000. Это предпочтительный способ.
Типичные ошибки кандидатов
-
Отсутствие ограничения
Без лимита сервер может принять огромное количество соединений, что приведёт к исчерпанию файловых дескрипторов и памяти. -
Использование middleware для ограничения соединений, а не запросов
Middleware ограничивает количество одновременно обрабатываемых запросов, но TCP-соединения всё равно открываются и потребляют ресурсы. Лучше ограничивать на уровне листенера или использоватьMaxConns. -
Забывают про keep-alive при подсчёте соединений
Каждое keep-alive соединение — это открытый сокет; лимит должен учитывать их. -
Неправильная конфигурация
MaxConnsPerIP
Может блокировать легитимных клиентов за NAT, если значение слишком мало.
Возможные дополнительные вопросы интервьюера
-
Как работают
http.Server.MaxConnsиMaxConnsPerIP?
Они ограничивают количество одновременных открытых соединений. При достижении лимита новые соединения не принимаются (Accept не вызывается). Добавлены в Go 1.22. -
Как сбрасывать лишние соединения с статусом 503?
Нельзя на этапе Accept; можно закрывать соединение сразу после Accept без чтения. -
Какие ещё методы ограничения?
Использоватьgolang.org/x/net/netutilсLimitListener.