HTTP: протокол, версии, коды, cookies, REST и gRPC
Кратко о теме
Заголовок раздела «Кратко о теме»HTTP — это прикладной протокол запрос-ответ без состояния (stateless), который исторически работает поверх надёжного транспорта: TCP для HTTP/1.0, 1.1 и 2, и QUIC поверх UDP для HTTP/3. Ключевая мысль, которую надо держать в голове: семантика HTTP (методы, коды статуса, заголовки, тело) одна и та же во всех версиях, а меняется только способ сериализации сообщений на проводе. HTTP/1.x — текстовая построчная сериализация с разделителями CRLF; HTTP/2 — бинарные фреймы и мультиплексирование потоков в одном TCP-соединении плюс сжатие заголовков HPACK; HTTP/3 — та же модель потоков, но поверх QUIC, где потоки независимы на транспортном уровне. Именно поэтому RFC 9110 («HTTP Semantics») отделён от RFC 9112/9113/9114, описывающих конкретные версии.
Второй опорный тезис — «без состояния» означает, что сервер не обязан помнить ничего между запросами: каждый запрос самодостаточен. Всё, что выглядит как состояние (сессия, авторизация, корзина), навешивается сверху — через cookies, заголовок Authorization, токены. Отсюда же растут REST-ограничения и удобство горизонтального масштабирования: любой запрос можно отдать любому инстансу за балансировщиком.
Третий тезис — HTTPS не является отдельным протоколом. Это тот же HTTP, но сообщения идут внутри TLS-туннеля: сначала TCP-рукопожатие, затем TLS-хендшейк с проверкой сертификата сервера и согласованием симметричного ключа, и только потом HTTP-запрос. TLS даёт три свойства: конфиденциальность (шифрование), целостность (AEAD/MAC) и аутентификацию сервера (цепочка сертификатов до доверенного корня). Порт по умолчанию 443 против 80, схема https:// против http://, а в HTTP/2 в браузерах TLS фактически обязателен (согласование версии идёт через ALPN).
Четвёртый тезис, который на собеседовании ценят выше знания номеров кодов, — любой сетевой вызов должен быть ограничен по времени. В Go http.Client{} по умолчанию не имеет общего таймаута вообще, а http.Server{} — таймаутов на чтение и запись; забыть их означает, что при зависшем пире горутина и соединение живут неограниченно долго, пул соединений и файловые дескрипторы утекают, и деградация одного downstream-сервиса каскадом валит ваш сервис. Правильная дисциплина — context.WithTimeout на каждый исходящий запрос плюс явные ReadHeaderTimeout/ReadTimeout/WriteTimeout/IdleTimeout на сервере.
Вопросы и ответы
Заголовок раздела «Вопросы и ответы»https://easyoffer.ru/questions/golang-developer
Заголовок раздела «https://easyoffer.ru/questions/golang-developer»Коротко. Это не вопрос, а ссылка на агрегатор вопросов с собеседований, попавшая в выгрузку. Обрывок исходника, вопрос не восстанавливается.
Глубже. По смыслу такие ссылки в конспектах интервьюеров означают «пройдись по типовому списку вопросов Go-разработчика». Практическая польза от них есть: easyoffer собирает частотность вопросов по реальным собеседованиям, и её стоит использовать как чек-лист покрытия тем, а не как источник ответов.
https://github.com/goavengers/go-interview
Заголовок раздела «https://github.com/goavengers/go-interview»Коротко. Ссылка на репозиторий-шпаргалку, не вопрос. Обрывок исходника, вопрос не восстанавливается.
Коротко. Ссылка на статью с Хабра, не вопрос. Обрывок исходника, вопрос не восстанавливается.
Коротко. Ссылка на статью с Хабра, не вопрос. Обрывок исходника, вопрос не восстанавливается.
https://kata.academy/blog/hrhunting/voprosy-po-go-na-sobesedovanii
Заголовок раздела «https://kata.academy/blog/hrhunting/voprosy-po-go-na-sobesedovanii»Коротко. Ссылка на подборку вопросов, не вопрос. Обрывок исходника, вопрос не восстанавливается.
https://sky.pro/wiki/profession/voprosy-dlya-sobesedovaniya-po-golang/
Заголовок раздела «https://sky.pro/wiki/profession/voprosy-dlya-sobesedovaniya-po-golang/»Коротко. Ссылка на подборку вопросов, не вопрос. Обрывок исходника, вопрос не восстанавливается.
Техничка: Дали вот этот код https://shorturl.at/KaVdM . Присылают за 1-2 дня, есть время проглядеть. На созвоне шаришь экран и комментируешь замечания, может какой-то код добавляешь.
Заголовок раздела «Техничка: Дали вот этот код https://shorturl.at/KaVdM . Присылают за 1-2 дня, есть время проглядеть. На созвоне шаришь экран и комментируешь замечания, может какой-то код добавляешь.»Коротко. Это описание формата секции — code review вслух по присланному заранее коду, а не вопрос. Готовиться к нему надо как к разбору чужого HTTP-сервиса: заранее составить чек-лист и идти по нему сверху вниз, комментируя вслух.
Глубже. Рабочий каркас чек-листа для типичного Go-HTTP-сервиса: (1) контекст — пробрасывается ли r.Context() в БД и в исходящие вызовы, есть ли таймауты на клиенте и сервере; (2) ресурсы — закрывается ли resp.Body, дочитывается ли он до конца перед закрытием (иначе соединение не вернётся в пул), закрываются ли rows, file; (3) ошибки — не проглатываются ли, оборачиваются ли через %w, не утекает ли внутренняя информация в тело ответа; (4) конкурентность — гонки на общих мапах, sync.Mutex vs sync.RWMutex, утечки горутин без завершения; (5) HTTP-специфика — коды статуса адекватны семантике, Content-Type выставляется до WriteHeader, тело не пишется после WriteHeader, ограничен ли размер тела через http.MaxBytesReader; (6) валидация входа и SQL-инъекции; (7) тесты и httptest. Отдельно ценится, когда кандидат отделяет «это баг» от «это вкусовщина» и предлагает конкретный патч, а не абстрактную критику.
Что такое опция HttpOnly у cookie?
Заголовок раздела «Что такое опция HttpOnly у cookie?»Коротко. HttpOnly — флаг в заголовке Set-Cookie, который запрещает доступ к cookie из JavaScript (document.cookie). Браузер по-прежнему отправляет такую cookie в HTTP-запросах, но скрипт на странице её не прочитает — это защита сессионного токена от кражи при XSS.
Глубже. HttpOnly закрывает только вектор «скрипт прочитал и отправил токен наружу»; он не защищает от CSRF (браузер всё равно приложит cookie к запросу) — от CSRF работает SameSite=Lax/Strict и/или CSRF-токен, и не защищает от перехвата в сети — для этого нужен флаг Secure (отправлять только по HTTPS). Практический набор для сессионной cookie: HttpOnly; Secure; SameSite=Lax; Path=/, а срок жизни — через Max-Age. В Go это поля структуры http.Cookie:
http.SetCookie(w, &http.Cookie{ Name: "session", Value: token, Path: "/", MaxAge: int((24 * time.Hour).Seconds()), HttpOnly: true, Secure: true, SameSite: http.SameSiteLaxMode,})Какой HTTP-код следует использовать для перенаправления пользователя с короткой ссылки на длинную?
Заголовок раздела «Какой HTTP-код следует использовать для перенаправления пользователя с короткой ссылки на длинную?»Коротко. Для сокращателя ссылок обычно берут 302 Found или 307 Temporary Redirect — временный редирект, чтобы браузер не кэшировал соответствие навсегда и каждый переход доходил до сервиса (это нужно для аналитики и для возможности переназначить ссылку). 301 Moved Permanently уместен только если ссылка гарантированно неизменна и нагрузку хочется снять кэшем браузера.
Глубже. Разница внутри пар важна: 301/302 исторически позволяют клиенту сменить метод на GET при редиректе, а 308/307 обязаны сохранить исходный метод и тело. Для сокращателя это почти не имеет значения (запросы GET), поэтому чаще всего пишут 302. Главный практический аргумент против 301 — браузеры кэшируют его агрессивно и надолго, и после смены цели ссылки пользователи продолжают уходить на старый адрес. В Go достаточно http.Redirect(w, r, longURL, http.StatusFound) — он сам выставит Location и напишет короткое тело.
Где в HTTP-ответе указывается адрес для перенаправления?
Заголовок раздела «Где в HTTP-ответе указывается адрес для перенаправления?»Коротко. В заголовке ответа Location. Клиент, получив код 3xx, читает Location и повторяет запрос по этому адресу.
Глубже. Location может содержать как абсолютный URI, так и относительную ссылку — клиент разрешает её относительно исходного запроса (RFC 9110). Заголовок используется не только в редиректах: при 201 Created в Location принято класть URI созданного ресурса. В Go http.Redirect выставляет Location и пишет минимальное HTML-тело для GET; на клиенте http.Client по умолчанию сам следует редиректам (до 10 переходов), а отключить это можно, задав CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }.
General questions to understand overall motivation and interest:
Заголовок раздела «General questions to understand overall motivation and interest:»Коротко. Это заголовок секции из плана интервью, а не вопрос. Обрывок исходника, вопрос не восстанавливается.
Глубже. Если такая секция реально идёт первой на собеседовании, интервьюер хочет услышать связную историю: почему вы смотрите на эту компанию/продукт, что именно в текущей работе перестало давать рост, какие задачи хотите решать дальше. Каркас ответа: контекст (где сейчас и чем занимаетесь) → чего не хватает → почему именно этот продукт/стек закрывает нехватку → что вы уже сделали в эту сторону. Ошибка — отвечать «хочу больше денег и интересных задач» без конкретики.
Из каких частей состоит HTTP запрос?
Заголовок раздела «Из каких частей состоит HTTP запрос?»Коротко. Из стартовой строки (метод, target/путь, версия протокола), набора заголовков «имя: значение», пустой строки-разделителя и необязательного тела. Формально: request line + headers + CRLF + body.
Глубже. В HTTP/1.1 это выглядит буквально так, байт в байт:
POST /api/v1/users?debug=1 HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/json\r\nContent-Length: 27\r\n\r\n{"name":"Ann","age":30}Разделитель строк — CRLF (\r\n), пустая строка отделяет заголовки от тела. Длина тела задаётся либо Content-Length, либо Transfer-Encoding: chunked (тогда тело идёт кусками, и длина заранее неизвестна). В HTTP/2 и HTTP/3 «стартовой строки» как таковой нет: метод, схема, :authority и путь передаются как псевдозаголовки :method, :scheme, :authority, :path в HEADERS-фрейме, а тело — в DATA-фреймах; семантика при этом та же. В Go всё это разложено по полям http.Request: Method, URL, Proto, Header, Body, а Host вынесен в отдельное поле, а не лежит в Header.
В чем отличия HTTP/HTTPS?
Заголовок раздела «В чем отличия HTTP/HTTPS?»Коротко. HTTPS — это тот же HTTP, но передаваемый внутри TLS-соединения. Отличия: шифрование и целостность данных, аутентификация сервера по сертификату, порт 443 вместо 80, схема https://, и дополнительная стоимость на TLS-хендшейк при установке соединения.
Глубже. Важно проговаривать, что именно защищено, а что нет. Внутри TLS скрыты: метод, путь, заголовки (включая cookies и Authorization), тело. Снаружи видны: IP-адреса и порты, размер и тайминг пакетов, DNS-запрос имени (если не используется DoH/DoT) и имя хоста в SNI на этапе ClientHello — до появления Encrypted Client Hello его видит любой наблюдатель на пути. Современный TLS 1.3 сократил хендшейк до одного round trip (1-RTT), с 0-RTT для возобновляемых сессий, так что «HTTPS медленный» — сегодня в основном миф; накладные расходы на симметричное шифрование с AES-NI пренебрежимо малы.
Зачем нужны таймауты в HTTP запрос?
Заголовок раздела «Зачем нужны таймауты в HTTP запрос?»Коротко. Чтобы вызов гарантированно завершался: без таймаута зависший или медленно отвечающий сервер удерживает горутину, соединение и файловый дескриптор бесконечно, и отказ одного downstream-сервиса каскадом исчерпывает ресурсы вашего. Таймаут превращает «висим навсегда» в предсказуемую ошибку, которую можно обработать — ретраем, фолбэком или деградацией.
Глубже. В Go это особенно важно, потому что http.Client{} по умолчанию имеет Timeout == 0, то есть без ограничений; ограничен только DialContext таймаутом ОС на установку TCP. Плохая практика — полагаться на «сервер сам ответит». Минимально корректный клиент:
client := &http.Client{ Timeout: 5 * time.Second, // на всё: dial + TLS + запрос + чтение тела Transport: &http.Transport{ DialContext: (&net.Dialer{Timeout: 2 * time.Second, KeepAlive: 30 * time.Second}).DialContext, TLSHandshakeTimeout: 2 * time.Second, ResponseHeaderTimeout: 3 * time.Second, MaxIdleConnsPerHost: 100, IdleConnTimeout: 90 * time.Second, },}req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)resp, err := client.Do(req)Отдельно помните: client.Timeout покрывает и чтение тела, поэтому для стриминговых ответов его выставлять нельзя — там нужен контекст с отменой и ResponseHeaderTimeout.
Зачем нужны таймауты в http-запросах, и как их подобрать?
Заголовок раздела «Зачем нужны таймауты в http-запросах, и как их подобрать?»Коротко. См. выше про назначение таймаутов. Подбирают их от целевого SLA сверху вниз: берут бюджет ответа своего эндпоинта, вычитают собственную обработку и делят остаток между зависимостями, ориентируясь на p99 их латентности с запасом примерно ×1.5–2, а не на среднее.
Глубже. Практическая методика: (1) измерить распределение латентности зависимости (p50/p95/p99) по метрикам, а не по ощущениям; (2) поставить таймаут чуть выше p99 — так вы отрезаете хвост, но не рубите нормальные запросы; (3) убедиться, что сумма таймаутов вниз по цепочке меньше таймаута вызывающего, иначе клиент отвалится раньше и вся работа будет выброшена; (4) использовать сквозной дедлайн: пробрасывать context.WithTimeout от входящего запроса, тогда каждый следующий вызов автоматически получает остаток бюджета. Таймаут почти всегда идёт в паре с ретраями (только для идемпотентных операций, с экспоненциальной задержкой и jitter) и с circuit breaker, иначе ретраи по таймауту сами устраивают шторм на уже перегруженный сервис.
Зачем нужны коды HTTP и какие они бывают?
Заголовок раздела «Зачем нужны коды HTTP и какие они бывают?»Коротко. Код статуса — машиночитаемый итог обработки запроса: он говорит клиенту, кэшу и прокси, что делать дальше, без разбора тела. Пять классов: 1xx — информационные, 2xx — успех, 3xx — перенаправление, 4xx — ошибка клиента, 5xx — ошибка сервера.
Глубже. Смысл классов важнее конкретных номеров: по первой цифре клиент принимает решения, даже если код ему незнаком (RFC 9110 прямо это предписывает). Разграничение 4xx/5xx определяет, кто виноват и надо ли ретраить: 4xx — запрос повторять бессмысленно, пока клиент его не изменит (исключения — 408, 425, 429), 5xx — проблема на стороне сервера, ретрай с backoff уместен. Ходовой набор: 200 OK, 201 Created, 202 Accepted, 204 No Content, 206 Partial Content; 301/302/304/307/308; 400/401/403/404/405/409/410/415/422/429; 500/501/502/503/504. Отдельно стоит знать 401 («не аутентифицирован», обязателен заголовок WWW-Authenticate) против 403 («аутентифицирован, но не разрешено»), 429 Too Many Requests с Retry-After и 503 Service Unavailable для временной недоступности.
Что такое HTTPS и для чего нужен?
Заголовок раздела «Что такое HTTPS и для чего нужен?»Коротко. HTTPS — это HTTP поверх TLS. Он нужен для трёх вещей: скрыть содержимое трафика от посторонних, гарантировать, что данные не подменили по пути, и убедиться, что вы разговариваете именно с тем сервером, чьё имя в адресной строке.
Глубже. Без TLS любой узел на пути (Wi-Fi-точка, провайдер, прозрачный прокси) может прочитать cookie сессии и подменить содержимое ответа — классическая атака «человек посередине» и инъекция скриптов в HTML. Помимо этого HTTPS сегодня — техническое требование: HTTP/2 и HTTP/3 в браузерах работают только по TLS, а часть Web API (Service Workers, геолокация, WebCrypto, HTTP/2 Server Push) доступны только в «secure context». Для принуждения к HTTPS используется заголовок Strict-Transport-Security (HSTS), который запрещает браузеру ходить на этот домен по обычному HTTP.
Этапы HTTP запроса?
Заголовок раздела «Этапы HTTP запроса?»Коротко. Резолв имени в IP через DNS → установка TCP-соединения (three-way handshake) → при HTTPS ещё TLS-хендшейк → отправка запроса (строка, заголовки, тело) → обработка на сервере → получение ответа (статус, заголовки, тело) → закрытие или удержание соединения в keep-alive для последующих запросов.
Глубже. Детали, которые ждут услышать на уровне middle+: DNS может отдать несколько адресов и клиент выберет один (в Go — net.Resolver, с Happy Eyeballs для IPv4/IPv6); TCP-хендшейк — это один RTT, TLS 1.3 — ещё один (TLS 1.2 — два); ALPN во время TLS-хендшейка согласует версию HTTP (h2 или http/1.1); при keep-alive следующие запросы к тому же хосту переиспользуют соединение и пропускают первые три шага — в Go это делает пул внутри http.Transport. Важная деталь Go: чтобы соединение вернулось в пул, тело ответа надо дочитать до конца и закрыть, иначе Transport порвёт соединение и следующий запрос заплатит за новый хендшейк.
HTTPS vs HTTP?
Заголовок раздела «HTTPS vs HTTP?»Коротко. См. выше про отличия HTTP и HTTPS: это один и тот же прикладной протокол, но HTTPS оборачивает его в TLS, работает по порту 443 и даёт шифрование, целостность и аутентификацию сервера.
Глубже. Единственное дополнение, которое стоит проговорить в такой краткой формулировке: HTTPS ничего не говорит о безопасности самого приложения. Валидный сертификат подтверждает только владение доменом (для DV-сертификатов), а не добросовестность владельца, и не защищает от XSS, SQL-инъекций или утечки данных из логов.
Зачем нужен таймаут в HTTP запросе и как его подобрать?
Заголовок раздела «Зачем нужен таймаут в HTTP запросе и как его подобрать?»Коротко. См. выше — таймаут ограничивает время удержания ресурсов и превращает зависание в обрабатываемую ошибку; подбирается от бюджета SLA вниз, ориентируясь на p99 зависимости.
Глубже. Дополнение про серверную сторону, которую в двух предыдущих вопросах часто забывают. У http.Server в Go таймауты по умолчанию тоже нулевые, и это прямой вектор Slowloris-атаки: клиент открывает соединения и шлёт заголовки по байту, удерживая горутины. Разумный минимум:
srv := &http.Server{ Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 15 * time.Second, WriteTimeout: 30 * time.Second, IdleTimeout: 60 * time.Second, MaxHeaderBytes: 1 << 20,}Для эндпоинтов со стримингом или загрузкой больших файлов WriteTimeout/ReadTimeout глобально ставить нельзя — либо выносите их на отдельный сервер, либо управляйте дедлайном через http.ResponseController (Go 1.20+): http.NewResponseController(w).SetWriteDeadline(time.Time{}).
200 OK - порядок
Заголовок раздела «200 OK - порядок»Коротко. 200 OK — стандартный код успешной обработки запроса, при котором в ответе есть тело с результатом. Это часть чек-листа «какие коды должен возвращать сервис», а не вопрос как таковой.
Глубже. Тонкости, за которые цепляются на собеседовании: 200 уместен для GET, PUT, PATCH, POST с телом ответа; если тела нет — корректнее 204 No Content; если запрос создал ресурс — 201 Created с Location; если работа принята в асинхронную обработку — 202 Accepted. Частая ошибка проектирования — отдавать 200 OK с телом {"error": "..."}: это ломает клиентов, кэши и мониторинг, потому что по коду ошибка невидима.
400 Bad Request - Некорректный запрос. Например, отсутствует необходимое поле или запрос имеет не валидный JSON формат.
Заголовок раздела «400 Bad Request - Некорректный запрос. Например, отсутствует необходимое поле или запрос имеет не валидный JSON формат.»Коротко. 400 Bad Request — сервер не смог обработать запрос из-за ошибки клиента в его форме: битый JSON, отсутствующее обязательное поле, некорректный тип значения. Повторять такой запрос без изменений бессмысленно.
Глубже. Стоит разделять «синтаксически невалидно» и «синтаксически валидно, но не проходит бизнес-валидацию»: для второго часть команд использует 422 Unprocessable Content, для неподдерживаемого Content-Type — 415 Unsupported Media Type, для конфликта состояния — 409 Conflict. Единого правила нет, но внутри одного API схема должна быть последовательной и задокументированной. В теле полезно отдавать машиночитаемую структуру ошибки — например, по RFC 9457 (application/problem+json) с полями type, title, status, detail, — и никогда не класть туда стектрейсы и SQL-запросы.
403 Forbidden - Доступ запрещен. Попытка обратиться к ресурсу, принадлежащему другому пользователю.
Заголовок раздела «403 Forbidden - Доступ запрещен. Попытка обратиться к ресурсу, принадлежащему другому пользователю.»Коротко. 403 Forbidden — клиент опознан, но прав на эту операцию у него нет; повторная аутентификация не поможет. Противопоставляется 401 Unauthorized, который означает «личность не установлена или креды невалидны».
Глубже. В сценарии «обращение к чужому ресурсу» (классический IDOR) есть выбор между 403 и 404: 404 не раскрывает сам факт существования объекта и потому предпочтителен там, где идентификаторы перебираемы, а утечка «такой заказ существует» уже является утечкой. 401 обязан сопровождаться заголовком WWW-Authenticate с указанием схемы (Bearer, Basic). И запомните историческую путаницу в названиях: 401 называется «Unauthorized», хотя по смыслу это «Unauthenticated».
404 Not Found - Запрашиваемый ресурс не найден.
Заголовок раздела «404 Not Found - Запрашиваемый ресурс не найден.»Коротко. 404 Not Found — сервер не нашёл ресурс по указанному URI и не сообщает, существовал ли он когда-либо. Для «существовал и удалён навсегда» есть более точный 410 Gone.
Глубже. Практические нюансы: 404 кэшируется по умолчанию как «эвристически кэшируемый», так что для динамических API его стоит явно помечать Cache-Control: no-store, если ресурс может появиться. Не путайте «маршрут не зарегистрирован» и «сущность не найдена в БД» — оба дают 404, но в теле полезно различать их для отладки. Если URI существует, а метод не поддерживается — это 405 Method Not Allowed с обязательным заголовком Allow; в Go начиная с 1.22 http.ServeMux умеет маршрутизацию по методу (mux.HandleFunc("GET /items/{id}", h)) и сам отдаёт 405 с Allow.
Сервис всегда возвращает данные в формате JSON, кроме случаев, когда явно необходим другой формат данных.
Заголовок раздела «Сервис всегда возвращает данные в формате JSON, кроме случаев, когда явно необходим другой формат данных.»Коротко. Это пункт из тестового задания, а не вопрос: требование отдавать Content-Type: application/json для всех ответов, включая ошибки, и отступать от него только там, где нужен другой формат (например, отдача файла как application/octet-stream или text/csv).
Глубже. Как это делают в Go правильно: заголовок выставляется до первой записи в тело и до WriteHeader, иначе Go применит определение типа по содержимому (http.DetectContentType) и заголовок уже не изменить.
func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) if err := json.NewEncoder(w).Encode(v); err != nil { // тело уже частично отправлено — только логируем log.Printf("write json: %v", err) }}Тонкость: json.NewEncoder(w).Encode пишет прямо в сокет, поэтому при ошибке маршалинга статус уже отправлен и исправить его нельзя; если структура сложная и есть риск ошибки — маршалите в буфер (json.Marshal), проверяйте ошибку, ставьте Content-Length и только потом пишите. Согласование форматов по запросу клиента делается через заголовок Accept (content negotiation) с ответом 406 Not Acceptable, если ни один вариант не поддерживается.
Реализуйте методы API для получения списка всех закаченных файлов;
Заголовок раздела «Реализуйте методы API для получения списка всех закаченных файлов;»Коротко. GET /files возвращает 200 OK и JSON-массив метаданных (id, имя, размер, mime, время загрузки), с пагинацией через query-параметры limit/offset или курсор. Тело в запросе отсутствует, операция идемпотентна и кэшируема.
Глубже. Минимальная реализация на стандартной библиотеке Go 1.22+, где ServeMux уже умеет метод в шаблоне:
type FileMeta struct { ID string `json:"id"` Name string `json:"name"` Size int64 `json:"size"` MIME string `json:"mime"` CreatedAt time.Time `json:"created_at"`}
func (s *Server) listFiles(w http.ResponseWriter, r *http.Request) { limit := 50 if v := r.URL.Query().Get("limit"); v != "" { n, err := strconv.Atoi(v) if err != nil || n <= 0 || n > 1000 { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid limit"}) return } limit = n } items, err := s.store.List(r.Context(), limit) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal"}) return } writeJSON(w, http.StatusOK, items) // пустой список -> [], а не null}
// mux.HandleFunc("GET /files", s.listFiles)Что оценивают: пагинация есть (иначе список однажды не влезет в память), пустой результат сериализуется как [], а не null (в Go nil-слайс маршалится в null — инициализируйте items := []FileMeta{}), контекст запроса пробрасывается в хранилище, ошибки хранилища не утекают наружу текстом.
Реализуйте методы API для удаления файлов;
Заголовок раздела «Реализуйте методы API для удаления файлов;»Коротко. DELETE /files/{id}: успешное удаление — 204 No Content (или 200 OK с телом), несуществующий id — 404 Not Found. Метод должен быть идемпотентным: повторный DELETE того же id не считается ошибкой сервера.
Глубже. Спорный момент, который любят обсуждать: что возвращать на повторное удаление. По RFC метод идемпотентен по эффекту, а не по коду ответа, поэтому оба варианта («всегда 204» и «404 на второй раз») допустимы — важно выбрать и задокументировать.
func (s *Server) deleteFile(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") // Go 1.22+ if id == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "empty id"}) return } switch err := s.store.Delete(r.Context(), id); { case err == nil: w.WriteHeader(http.StatusNoContent) case errors.Is(err, ErrNotFound): writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"}) default: writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal"}) }}
// mux.HandleFunc("DELETE /files/{id}", s.deleteFile)Отдельно: id из пути нельзя подставлять в путь на диске напрямую — это path traversal (../../etc/passwd). Храните файлы под сгенерированными именами (UUID) и валидируйте id регуляркой; при удалении сначала удаляйте запись из БД, потом файл, и делайте операцию устойчивой к повтору.
Реализуйте работу сервера по протоколу HTTPS.
Заголовок раздела «Реализуйте работу сервера по протоколу HTTPS.»Коротко. В Go достаточно srv.ListenAndServeTLS(certFile, keyFile) — сертификат и приватный ключ в PEM. В проде обычно TLS терминируется на балансировщике/ingress, а сертификаты выпускаются автоматически (ACME/Let’s Encrypt), но уметь поднять TLS на самом сервисе надо.
Глубже. Полный пример с явной конфигурацией TLS и редиректом с HTTP:
srv := &http.Server{ Addr: ":8443", Handler: mux, ReadHeaderTimeout: 5 * time.Second, TLSConfig: &tls.Config{ MinVersion: tls.VersionTLS12, },}log.Fatal(srv.ListenAndServeTLS("server.crt", "server.key"))Самоподписанный сертификат для локальной разработки: openssl req -x509 -newkey rsa:2048 -nodes -keyout server.key -out server.crt -days 365 -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost" — обязательно с SAN, потому что современные клиенты (и Go начиная с 1.15) игнорируют CN и требуют subjectAltName. Для автоматического получения сертификатов есть golang.org/x/crypto/acme/autocert. Взаимную аутентификацию (mTLS) включают через ClientAuth: tls.RequireAndVerifyClientCert и ClientCAs. Не забудьте HandleFunc на 80-м порту с http.Redirect(..., http.StatusMovedPermanently) и заголовок HSTS.
Что такое http и https? В чем разница?
Заголовок раздела «Что такое http и https? В чем разница?»Коротко. См. выше про HTTP и HTTPS: HTTP — текстовый/бинарный протокол запрос-ответ прикладного уровня, HTTPS — он же, но внутри TLS-туннеля, с шифрованием, проверкой целостности и аутентификацией сервера по сертификату.
Глубже. Дополнение, которое отличает уверенный ответ: в браузере переход на https:// меняет не только транспорт, но и модель безопасности — появляется secure context, начинают действовать Secure-cookies, работает HTTP/2 и HTTP/3, а смешанный контент (http://-ресурсы на https://-странице) блокируется.
Вопрос про HTTP протоколы. Что знаешь о протоколе HTTP?
Заголовок раздела «Вопрос про HTTP протоколы. Что знаешь о протоколе HTTP?»Коротко. HTTP — прикладной протокол запрос-ответ, stateless, текстовый в версии 1.x и бинарный начиная с 2.0, работает поверх TCP (HTTP/3 — поверх QUIC/UDP). Определяет методы, коды статуса, заголовки и правила кэширования; всё состояние навешивается сверху через cookies и токены.
Глубже. Структурированный ответ на такой открытый вопрос стоит строить по осям: (1) семантика — методы, их идемпотентность и безопасность, коды статуса, заголовки, согласование контента; (2) версии — 0.9/1.0/1.1/2/3 и что в каждой изменилось на уровне провода; (3) соединения — keep-alive, пайплайнинг (мёртв), мультиплексирование, head-of-line blocking; (4) кэширование — Cache-Control, ETag, Last-Modified, условные запросы и 304; (5) безопасность — TLS, HSTS, CORS, cookies-флаги. Если пройти по этим пяти пунктам за две минуты, дальше интервьюер сам выберет, куда углубляться.
Реализовать фильтрацию продуктов по цене и заголовку наиболее быстрым способом
Заголовок раздела «Реализовать фильтрацию продуктов по цене и заголовку наиболее быстрым способом»Коротко. Быстрее всего — не фильтровать в приложении, а отдать работу хранилищу: диапазон по цене закрывается B-tree индексом, поиск по заголовку — полнотекстовым индексом (GIN + tsvector в PostgreSQL) или триграммным (pg_trgm) для подстроки. В API это GET /products?price_min=&price_max=&q= с пагинацией по курсору.
Глубже. Если по условию задачи данные уже в памяти сервиса, «наиболее быстрый способ» — предподготовленные структуры вместо линейного прохода на каждый запрос: отсортированный по цене слайс с sort.Search (двоичный поиск границ диапазона за O(log n)) и инвертированный индекс map[string][]int по нормализованным токенам заголовка, затем пересечение двух множеств от меньшего к большему.
// products отсортированы по Pricelo := sort.Search(len(products), func(i int) bool { return products[i].Price >= min })hi := sort.Search(len(products), func(i int) bool { return products[i].Price > max })candidates := products[lo:hi] // O(log n) на поиск границЧто ждут услышать: сложность до и после (O(n) на запрос против O(log n + k)), где хранится состояние и как оно обновляется при записи, что делать с пагинацией (offset деградирует на больших смещениях — используйте keyset/seek-пагинацию по (price, id)), и что «наиболее быстро» без профилирования — гипотеза; на 10 000 записей линейный проход по слайсу структур может обогнать любые индексы за счёт локальности кэша.
Пользователь отправил HTTP-запрос и ждет ответа. Сервис выполняет долгую операцию. Почему не стоит позволять запросу выполняться неограниченно долго?
Заголовок раздела «Пользователь отправил HTTP-запрос и ждет ответа. Сервис выполняет долгую операцию. Почему не стоит позволять запросу выполняться неограниченно долго?»Коротко. Потому что ресурсы под этот запрос удерживаются всё время его жизни: горутина, TCP-соединение, дескриптор, соединение к БД из пула. Клиент при этом почти наверняка уже отвалился по своему таймауту или закрыл вкладку, и работа делается впустую, а под нагрузкой такие «висяки» накапливаются и исчерпывают пулы — сервис перестаёт обслуживать даже быстрые запросы.
Глубже. Ключевая деталь для Go: когда клиент разрывает соединение, r.Context() отменяется, и правильно написанный обработчик это увидит — но только если он реально передаёт контекст вниз (в db.QueryContext, в http.NewRequestWithContext) и проверяет ctx.Err() в длинных циклах. Если контекст игнорируется, работа продолжается «в никуда». Архитектурно долгие операции не должны жить внутри синхронного HTTP-запроса вообще: правильный паттерн — принять задачу, вернуть 202 Accepted с идентификатором и Location на ресурс статуса, выполнять в фоне (воркер, очередь), а клиент опрашивает статус или получает вебхук. Плюс к этому — ограничение конкурентности (семафор/пул воркеров), чтобы всплеск запросов не породил неограниченное число фоновых задач.
Can you do streaming on gRPC with a regular HTTP client?
Заголовок раздела «Can you do streaming on gRPC with a regular HTTP client?»Коротко. Обычным http.Client теоретически можно — gRPC это HTTP/2 с телом в специфичном формате кадров (1 байт флага сжатия + 4 байта длины + protobuf), и Go-шный http.Client умеет HTTP/2 и потоковые тела через io.Pipe. Но на практике так не делают: придётся вручную реализовывать фрейминг, trailers с grpc-status, дедлайны, сжатие и обработку ошибок — проще взять google.golang.org/grpc.
Глубже. Что действительно нужно от клиента: поддержка HTTP/2 с полным дуплексом (запрос ещё пишется, а ответ уже читается), поддержка HTTP-trailers (gRPC кладёт grpc-status/grpc-message именно в трейлеры после тела) и Content-Type: application/grpc+proto. Go-шный http.Transport с ForceAttemptHTTP2 дуплекс поддерживает, трейлеры читает через resp.Trailer после исчерпания тела — так что технически всё есть. Ограничения появляются в браузере: fetch/XHR не дают контроля над HTTP/2-фреймами и трейлерами, поэтому из браузера используют gRPC-Web через прокси (Envoy, grpcwebproxy), а он не поддерживает клиентский и двунаправленный стриминг — только unary и server-streaming.
Can you build a REST API from a protofile?
Заголовок раздела «Can you build a REST API from a protofile?»Коротко. Да. Стандартный путь — grpc-gateway: в .proto через опции google.api.http описывается маппинг RPC-методов на HTTP-методы и пути, кодогенератор строит reverse-proxy, который принимает JSON поверх REST и транслирует в gRPC. Альтернативы — Connect (connectrpc.com/connect), который сам умеет отдавать и gRPC, и HTTP/JSON из одного сервера, и Envoy с транскодингом gRPC-JSON.
Глубже. Пример аннотации в proto:
service Users { rpc GetUser(GetUserRequest) returns (User) { option (google.api.http) = { get: "/v1/users/{id}" }; }}Из того же файла генерируется и OpenAPI-спека (protoc-gen-openapiv2), так что контракт остаётся единственным источником правды. Подводные камни: маппинг protobuf↔JSON регулируется protojson (поля в lowerCamelCase, enum строками, int64 строкой), поля со значением по умолчанию по умолчанию опускаются в JSON — это часто ломает клиентов, ожидающих 0/false; отсутствие в proto3 различия «поле не задано» и «поле равно нулю» решают через optional (наличие presence) или wrapper-типы. И REST, получившийся автоматически, обычно «RPC в URL-обёртке», а не идиоматичный REST — если нужен настоящий resource-oriented API, маппинг придётся проектировать руками.
Как работает https и как работает сертификат?
Заголовок раздела «Как работает https и как работает сертификат?»Коротко. Клиент устанавливает TCP-соединение, затем TLS-хендшейк: посылает ClientHello (поддерживаемые шифры, SNI с именем хоста, ALPN), сервер отвечает своим сертификатом и участвует в обмене ключами (в TLS 1.3 — ECDHE), клиент проверяет цепочку сертификатов до доверенного корня и совпадение имени, стороны выводят общий симметричный ключ, и дальше весь HTTP идёт зашифрованным AEAD-шифром.
Глубже. Сертификат X.509 — это структура с публичным ключом сервера, именами (subjectAltName), сроком действия и подписью удостоверяющего центра. Проверка на клиенте включает: (1) построение цепочки от листового сертификата через промежуточные до корня из системного/встроенного хранилища доверия; (2) проверку каждой подписи публичным ключом вышестоящего; (3) срок действия; (4) соответствие имени хоста SAN-записям; (5) статус отзыва (CRL/OCSP, в браузерах — CRLite/OCSP stapling); (6) назначение ключа (extendedKeyUsage: serverAuth). Обладание приватным ключом сервер доказывает подписью в сообщении CertificateVerify (TLS 1.3) — иначе сертификат можно было бы просто скопировать. Важно: сам сертификат не шифрует данные, он только аутентифицирует сторону и даёт ключ для проверки подписи; шифрование трафика идёт симметричным ключом, выведенным из ECDHE-обмена, и обладает forward secrecy — компрометация приватного ключа сервера не расшифровывает записанный ранее трафик. В Go проверка цепочки — crypto/x509, (*x509.Certificate).Verify; отключение проверки через InsecureSkipVerify: true допустимо только в тестах.
Что такое http 2.0 и какие основные отличия?
Заголовок раздела «Что такое http 2.0 и какие основные отличия?»Коротко. HTTP/2 (RFC 9113) — бинарная версия того же HTTP: сообщения нарезаются на фреймы, много запросов мультиплексируются в одном TCP-соединении параллельно, заголовки сжимаются алгоритмом HPACK, есть приоритеты потоков и (в исходной спецификации) server push. Семантика методов и кодов не изменилась.
Глубже. Что решалось: в HTTP/1.1 на одно соединение можно вести только один запрос за раз (пайплайнинг практически не работал из-за head-of-line blocking на уровне ответов), поэтому браузеры открывали по 6 соединений на хост, а разработчики городили спрайты, конкатенацию и доменное шардирование. HTTP/2 убирает всё это: одно соединение, N потоков, каждый со своим ID, фреймы HEADERS/DATA/SETTINGS/WINDOW_UPDATE/RST_STREAM/GOAWAY. HPACK — статическая таблица частых заголовков плюс динамическая таблица уже переданных, что резко сокращает повторяющиеся Cookie/User-Agent. Оставшаяся проблема — HOL blocking на уровне TCP: потерянный сегмент задерживает доставку всех потоков сразу, потому что TCP гарантирует порядок для всего соединения; это и стало мотивацией HTTP/3. Server push оказался неудачным и удалён из Chrome; в Go http.Pusher формально есть, но использовать его не стоит. Практика в Go: net/http поддерживает HTTP/2 «из коробки» для TLS-серверов и клиентов; для h2c (без TLS) нужен golang.org/x/net/http2/h2c. В Go 1.24 появилась настройка Server.HTTP2/Transport.HTTP2 и защита от атаки Rapid Reset через ограничение числа одновременных потоков.
А про http 3.0 что можете рассказать?
Заголовок раздела «А про http 3.0 что можете рассказать?»Коротко. HTTP/3 (RFC 9114) — это HTTP/2-подобная модель потоков поверх QUIC, а QUIC работает поверх UDP. Главное отличие: потоки независимы на транспортном уровне, поэтому потеря пакета в одном потоке не тормозит остальные — head-of-line blocking уровня TCP исчезает. Плюс объединённый криптографический и транспортный хендшейк (1-RTT, а с 0-RTT — вообще без задержки при повторном подключении) и миграция соединения по Connection ID при смене IP (переход Wi-Fi → LTE не рвёт сессию).
Глубже. TLS 1.3 встроен в QUIC, незашифрованного HTTP/3 не существует. Сжатие заголовков — QPACK, вариант HPACK, устойчивый к внеочередной доставке. Обнаружение поддержки идёт через заголовок Alt-Svc: h3=":443" или через HTTPS-запись в DNS: клиент сначала подключается по HTTP/2 и запоминает, что можно перейти на h3. Минусы, которые стоит назвать: UDP местами блокируется корпоративными файрволами (нужен fallback на TCP), обработка идёт в user space и потому дороже по CPU, чем разгруженный в ядро TCP, а инструменты отладки менее зрелые. В Go в стандартной библиотеке HTTP/3 нет; на практике используют github.com/quic-go/quic-go и http3-обвязку к нему.
Чем отличается gRPC от HTTP
Заголовок раздела «Чем отличается gRPC от HTTP»Коротко. Сравнение некорректно по уровням: gRPC — это RPC-фреймворк, который использует HTTP/2 как транспорт. Отличается он от «обычного HTTP+JSON API» тем, что контракт описан в .proto и код генерируется, данные бинарные (protobuf), поддерживаются четыре вида вызовов (unary, server-stream, client-stream, bidi), и есть встроенные дедлайны, метаданные, статус-коды и интерсепторы.
Глубже. Практические следствия: protobuf компактнее и быстрее в парсинге, чем JSON, а строгая схема ловит расхождения контрактов на этапе компиляции; за это платят человекочитаемостью (curl не поможет, нужен grpcurl) и необходимостью инфраструктуры для генерации и распространения proto-файлов. gRPC не работает напрямую из браузера — нужен gRPC-Web и прокси. Балансировка тоже отличается: одно долгоживущее HTTP/2-соединение мультиплексирует все вызовы, поэтому L4-балансировщик распределяет соединения, а не запросы, и нагрузка перекашивается — нужен клиентский балансинг или L7-прокси (Envoy, Linkerd). Собственные коды статуса gRPC (OK, INVALID_ARGUMENT, NOT_FOUND, DEADLINE_EXCEEDED, UNAVAILABLE) едут в HTTP-трейлере, а HTTP-статус при этом почти всегда 200.
Что сложнее реализовать? Общение по gRPC или HTTP
Заголовок раздела «Что сложнее реализовать? Общение по gRPC или HTTP»Коротко. Простейший HTTP+JSON поднимается быстрее: в Go это net/http без единой зависимости, отладка через curl. gRPC требует toolchain (protoc/buf, плагины, кодогенерация, хранение и версионирование proto), но зато после настройки клиент и сервер получаются типобезопасными и рутинного кода меньше — на длинной дистанции и большом числе сервисов gRPC часто дешевле.
Глубже. Где сложность реально возникает: (1) распространение контрактов — отдельный репозиторий proto, buf-реестр, версионирование, правила обратной совместимости (нельзя менять номера полей и переиспользовать удалённые — их надо резервировать через reserved); (2) инфраструктура — балансировка долгоживущих HTTP/2-соединений, health checking, keepalive-настройки, поддержка в ingress; (3) наблюдаемость — бинарное тело не видно в логах, нужны интерсепторы и grpcurl с reflection; (4) браузерные клиенты — только через gRPC-Web/прокси. С HTTP+JSON сложность приходит с другой стороны: ручная сериализация, рассинхрон документации и кода, отсутствие типобезопасности между сервисами, «каждый API немного свой». Честный ответ на собеседовании — «стартовать проще на HTTP, поддерживать десятки внутренних сервисов проще на gRPC».
Какие протоколы поверх http ты знаешь?
Заголовок раздела «Какие протоколы поверх http ты знаешь?»Коротко. WebSocket (апгрейд с HTTP через Upgrade: websocket), gRPC и gRPC-Web, SSE (Server-Sent Events, text/event-stream), WebDAV и CalDAV/CardDAV, SOAP и XML-RPC/JSON-RPC, OData, а также прикладные поверх HTTP: OAuth 2.0/OIDC, ACME (Let’s Encrypt), S3 API, GraphQL over HTTP, DNS-over-HTTPS.
Глубже. Стоит различать три случая. Первый — протоколы, которые используют HTTP как транспорт сообщений, сохраняя модель запрос-ответ (JSON-RPC, SOAP, GraphQL, S3): это по сути соглашение о формате тела и URL. Второй — те, что расширяют семантику самого HTTP новыми методами и заголовками (WebDAV: PROPFIND, MKCOL, LOCK). Третий — те, что используют HTTP только для установления соединения и затем переходят на собственный протокол поверх того же TCP: WebSocket (101 Switching Protocols) и CONNECT-туннели прокси. SSE стоит особняком — это чистый HTTP-ответ с бесконечным телом в формате data: ...\n\n, односторонний сервер→клиент, зато переживает прокси и автоматически переподключается; в Go он делается через http.Flusher (Go 1.20+: http.NewResponseController(w).Flush()).
Backend (REST, Java/Kotlin или Go) - обработка входящих запросов и бизнес логика
Заголовок раздела «Backend (REST, Java/Kotlin или Go) - обработка входящих запросов и бизнес логика»Коротко. Это фрагмент описания вакансии или архитектуры системы, а не вопрос. Обрывок исходника, вопрос не восстанавливается.
Глубже. Если это преамбула к обсуждению архитектуры, ожидаемый разговор — про слои: транспортный слой (HTTP-хендлеры: парсинг, валидация, коды ответа), слой бизнес-логики (чистые сервисы без знания о HTTP), слой доступа к данным (репозитории). Ключевая мысль, которую ценят: http.Request и http.ResponseWriter не должны просачиваться в бизнес-логику — иначе её нельзя переиспользовать из gRPC-хендлера, консьюмера очереди или CLI и невозможно нормально тестировать.
Общие рекомендации по подготовке к собесу - https://job.ozon.ru/events/32
Заголовок раздела «Общие рекомендации по подготовке к собесу - https://job.ozon.ru/events/32»Коротко. Ссылка на материалы по подготовке, не вопрос. Обрывок исходника, вопрос не восстанавливается.
Что такое HTTP-протокол?
Заголовок раздела «Что такое HTTP-протокол?»Коротко. См. выше — прикладной протокол передачи гипертекста, построенный по модели «запрос-ответ», без состояния между запросами, поверх TCP (или QUIC для HTTP/3), с фиксированным набором методов, кодов статуса и заголовков.
Глубже. Единственное, что стоит добавить к предыдущим формулировкам: слово «гипертекст» в названии давно не отражает применение — HTTP сегодня универсальный транспорт для API, файлов, стриминга видео (HLS/DASH), телеметрии и даже DNS. И «протокол без состояния» относится к самому протоколу, а не к приложению: сервер вполне может иметь состояние, просто HTTP не обязывает его помнить предыдущие запросы клиента.
Чем HTTP-ответ отличается от HTTP-запроса?
Заголовок раздела «Чем HTTP-ответ отличается от HTTP-запроса?»Коротко. Структура одинаковая — стартовая строка, заголовки, пустая строка, тело, — но первая строка разная: в запросе это МЕТОД target HTTP/версия, в ответе HTTP/версия КОД причина (status line). Часть заголовков специфична для направления: Host, Accept, Authorization — у запроса; Server, Set-Cookie, Location, WWW-Authenticate — у ответа.
Глубже. В Go это отражено двумя типами: http.Request (поля Method, URL, Host, Body) и http.Response (StatusCode, Status, Body). Полезная деталь: http.Request используется и на сервере, и на клиенте, но семантика полей отличается — например, на сервере URL содержит только путь и query, а Host лежит в отдельном поле; на клиенте URL должен быть абсолютным. Тело у запроса опционально (GET обычно без тела), у ответа отсутствует для 204, 304 и для ответов на HEAD. Общие для обоих направлений заголовки — сущностные (Content-Type, Content-Length, Content-Encoding) и связи (Connection, Transfer-Encoding).
Что такое cookies и как они используются в HTTP?
Заголовок раздела «Что такое cookies и как они используются в HTTP?»Коротко. Cookie — небольшая пара «имя=значение», которую сервер отдаёт браузеру заголовком Set-Cookie, а браузер затем автоматически прикладывает к каждому подходящему запросу в заголовке Cookie. Это механизм добавления состояния (сессии, авторизации, настроек) поверх stateless-протокола.
Глубже. Атрибуты определяют область применения и безопасность: Domain и Path — куда отправлять; Expires/Max-Age — срок жизни (без них cookie сессионная и живёт до закрытия браузера); Secure — только по HTTPS; HttpOnly — недоступна из JS; SameSite=Strict|Lax|None — отправлять ли при кросс-сайтовых переходах (в современных браузерах по умолчанию Lax, а None требует Secure). Ограничения браузеров: около 4 КБ на cookie и порядка 50 штук на домен, поэтому в cookie кладут идентификатор сессии, а данные хранят на сервере. Два подхода: непрозрачный session id + серверное хранилище (легко отзывать, нужен Redis/БД) либо подписанный/зашифрованный токен внутри cookie (stateless, но отзыв сложен). В Go: r.Cookie("name"), r.Cookies(), http.SetCookie(w, c); на клиенте автоматическое хранение включается через http.Client{Jar: jar} с net/http/cookiejar.
Где хранятся cookies?
Заголовок раздела «Где хранятся cookies?»Коротко. На стороне клиента: браузер держит их в собственном хранилище (обычно SQLite-файл в профиле пользователя) и подставляет в заголовок Cookie при каждом запросе к подходящему домену и пути. Сервер cookie не хранит — он хранит связанные с ними данные сессии, если использует серверные сессии.
Глубже. Изоляция хранилища идёт по домену (с учётом Domain, Public Suffix List и, в современных браузерах, по partition key для сторонних cookie), а не по вкладке. В HTTP-клиентах вне браузера cookie не сохраняются автоматически: Go-шный http.Client без Jar просто игнорирует Set-Cookie, и это регулярный источник багов в интеграционных тестах авторизации. Сессионные cookie (без Expires/Max-Age) живут в памяти процесса браузера, персистентные — на диске. Для чувствительных значений важно помнить: cookie-файл читается любым процессом с правами пользователя, поэтому долгоживущие токены в cookie — компромисс, а не абсолютная защита.
Что такое cookies в контексте семантики HTTP-запроса?
Заголовок раздела «Что такое cookies в контексте семантики HTTP-запроса?»Коротко. См. выше про cookies. В контексте семантики запроса cookie — это обычный заголовок Cookie, то есть часть метаданных запроса; протокол не придаёт ему особого смысла, состояние в него вкладывает приложение.
Глубже. Семантическая тонкость, ради которой обычно и задают вопрос: cookie превращает формально идентичные запросы в разные по результату, и это ломает наивное кэширование — поэтому ответы, зависящие от cookie, обязаны нести Cache-Control: private (или no-store) и Vary, иначе общий кэш (CDN, прокси) отдаст одному пользователю персональный ответ другого. Вторая тонкость: cookie отправляется браузером автоматически, включая кросс-сайтовые запросы, — отсюда CSRF, и отсюда же SameSite. Третья: cookie не участвует в определении идемпотентности или безопасности метода — GET с cookie остаётся безопасным методом с точки зрения HTTP, даже если приложение внутри что-то меняет (что само по себе анти-паттерн).
REST/GRPC разница и когда какой применяется?
Заголовок раздела «REST/GRPC разница и когда какой применяется?»Коротко. REST — архитектурный стиль поверх HTTP с ресурсами, URI, стандартными методами и обычно JSON; gRPC — контрактный RPC на protobuf поверх HTTP/2 с кодогенерацией и стримингом. REST берут для публичных API, браузерных клиентов и там, где важны читаемость и совместимость с кэшами и прокси; gRPC — для внутреннего service-to-service общения, где важны производительность, строгий контракт и потоковые вызовы.
Глубже. Более точные критерии выбора. За REST: клиент — браузер или сторонний разработчик; нужно HTTP-кэширование и работа через любые прокси; нужна отладка curl’ом; API меняется публично и требует человекочитаемой документации. За gRPC: десятки внутренних сервисов, где рассинхрон контрактов дорог; высокий RPS и чувствительность к латентности и объёму трафика; нужны двунаправленные потоки; полиглотная среда, где генерация клиентов на нескольких языках экономит много ручной работы. Гибрид тоже нормален: gRPC внутри, REST-фасад наружу через grpc-gateway или Connect. Что не является аргументом: «gRPC быстрее» без цифр — на мелких сообщениях разница с JSON часто теряется на фоне сети и БД, а бенчмарк надо делать на своих данных.
HTTP работает поверх какого протокола?
Заголовок раздела «HTTP работает поверх какого протокола?»Коротко. Поверх TCP для версий 0.9, 1.0, 1.1 и 2 (по умолчанию порт 80, для HTTPS — TLS поверх TCP на порту 443). HTTP/3 работает поверх QUIC, а QUIC — поверх UDP.
Глубже. Требование к транспорту в спецификации сформулировано как «надёжный поток с сохранением порядка» — TCP ему удовлетворяет. QUIC даёт те же гарантии на уровне отдельного потока, но реализует их сам в user space поверх UDP, добавляя мультиплексирование независимых потоков и встроенный TLS 1.3. Полезно уметь назвать модель OSI/TCP-IP: HTTP — прикладной уровень (L7), TLS — между прикладным и транспортным (условно L6/представления), TCP/UDP — транспортный (L4), IP — сетевой (L3). Исторический курьёз: HTTP не привязан к TCP жёстко — существуют реализации поверх Unix-сокетов (Docker API) и in-memory-транспортов (в Go — httptest.Server и кастомный RoundTripper).
В чем разница между HTTP1, HTTP1.1 и HTTP2.0?
Заголовок раздела «В чем разница между HTTP1, HTTP1.1 и HTTP2.0?»Коротко. HTTP/1.0 — по одному запросу на TCP-соединение, соединение закрывается после ответа, заголовок Host необязателен. HTTP/1.1 — постоянные соединения (keep-alive по умолчанию), обязательный Host (виртуальный хостинг), Transfer-Encoding: chunked, кэширование через Cache-Control/ETag, диапазонные запросы, пайплайнинг (на практике не прижился). HTTP/2 — бинарный формат, мультиплексирование потоков в одном соединении, сжатие заголовков HPACK, приоритеты.
Глубже. Проблема, оставшаяся в HTTP/1.1, — head-of-line blocking на уровне ответов: соединение переиспользуется, но ответы должны идти строго в порядке запросов, поэтому один медленный ответ блокирует остальные. Пайплайнинг пытался слать несколько запросов не дожидаясь ответов, но из-за багов промежуточных прокси браузеры его отключили. Обходным путём стало открытие 6 параллельных соединений на хост и хаки уровня фронтенда (спрайты, инлайнинг, доменное шардирование) — все они в HTTP/2 становятся вредными и их надо убирать. HTTP/2 снимает HOL на уровне HTTP, но не на уровне TCP. Ещё различия: в HTTP/2 имена заголовков обязаны быть в нижнем регистре, а вместо стартовой строки используются псевдозаголовки :method, :path, :scheme, :authority; в Go net/http абстрагирует это, и один и тот же http.Handler работает поверх любой версии.
Какие заголовки являются обязательными?
Заголовок раздела «Какие заголовки являются обязательными?»Коротко. В HTTP/1.1 обязателен ровно один заголовок запроса — Host (без него сервер обязан вернуть 400). Всё остальное обязательно условно: если есть тело, нужен Content-Length либо Transfer-Encoding: chunked; в HTTP/2/3 роль Host играет псевдозаголовок :authority.
Глубже. «Условно обязательные» стоит перечислить: Content-Type — обязателен по смыслу, если тело есть и его формат неочевиден (без него сервер угадывает или отвечает 415); WWW-Authenticate — обязателен в ответе 401; Allow — в ответе 405; Location — в ответах 3xx и в 201 Created; Retry-After — рекомендован при 429 и 503; Date — сервер обязан его отдавать, если у него есть часы. У ответа формально обязательных заголовков нет вообще: HTTP/1.1 204 No Content\r\n\r\n — валидный ответ. В Go net/http сам добавляет Date, Content-Length (или chunked) и Content-Type по автоопределению, если вы его не задали.
Какие есть HTTP методы?
Заголовок раздела «Какие есть HTTP методы?»Коротко. GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS, TRACE, CONNECT. Ключевые свойства — безопасность (не изменяет состояние: GET, HEAD, OPTIONS, TRACE) и идемпотентность (повтор даёт тот же эффект: все безопасные плюс PUT и DELETE); неидемпотентны POST и PATCH.
Глубже. Свойства методов — не формальность, а практика: только безопасные методы разрешено автоматически ретраить и предзагружать, и только идемпотентные ретраит транспорт после сетевой ошибки (Go-шный http.Transport повторяет запрос, если соединение из пула оказалось разорвано, — но только когда тело можно перечитать и метод идемпотентен). Кэшируемы по умолчанию ответы на GET и HEAD; POST кэшируем только при явных заголовках. TRACE в проде обычно отключают (атака Cross-Site Tracing), CONNECT используется прокси для туннелирования TLS. Для неидемпотентных операций, которые нужно безопасно повторять, вводят ключ идемпотентности — заголовок вроде Idempotency-Key с серверным дедупом, — это стандартная практика в платёжных API.
В чем разнича между PUT и PATCH?
Заголовок раздела «В чем разнича между PUT и PATCH?»Коротко. PUT заменяет ресурс целиком телом запроса и идемпотентен; PATCH применяет частичное изменение и в общем случае неидемпотентен. Практически: PUT /users/1 с неполным телом должен обнулить отсутствующие поля, PATCH /users/1 меняет только переданные.
Глубже. Формат тела PATCH не задан протоколом — надо выбирать: JSON Merge Patch (RFC 7386, application/merge-patch+json, где null означает удаление поля) или JSON Patch (RFC 6902, application/json-patch+json, список операций add/remove/replace). Merge Patch не умеет различать «не передано» и «удалить» иначе как через null и не работает с элементами массивов; JSON Patch выразительнее, но многословнее. Идемпотентность PATCH зависит от операции: replace идемпотентен, а {"op":"add","path":"/tags/-","value":"x"} — нет. Для конкурентных изменений применяют условные запросы: клиент присылает If-Match: "etag", сервер отвечает 412 Precondition Failed, если ресурс изменился, — это оптимистическая блокировка средствами HTTP. Ещё одно отличие: PUT может создавать ресурс по указанному клиентом URI (201 Created), PATCH несуществующий ресурс обычно не создаёт (404).
Что делают методы HEAD и OPTIONS?
Заголовок раздела «Что делают методы HEAD и OPTIONS?»Коротко. HEAD — то же, что GET, но сервер возвращает только статус и заголовки без тела; используется для проверки существования ресурса, его размера (Content-Length) и свежести (ETag, Last-Modified) без скачивания. OPTIONS сообщает, какие возможности доступны для ресурса: в ответ идёт заголовок Allow со списком методов, а в браузерном мире OPTIONS — это CORS preflight.
Глубже. По HEAD: заголовки должны быть ровно те же, что вернул бы GET (включая Content-Length), поэтому сервер обязан «как бы» сформировать ответ. В Go при обработке HEAD net/http сам отбрасывает записанное в тело, а http.ServeContent/http.ServeFile обрабатывают его корректно. По OPTIONS: preflight-запрос браузер шлёт автоматически перед «непростым» кросс-доменным запросом (нестандартный метод, кастомные заголовки, Content-Type: application/json), с заголовками Origin, Access-Control-Request-Method, Access-Control-Request-Headers; сервер обязан ответить 204/200 с Access-Control-Allow-Origin, -Methods, -Headers и, желательно, Access-Control-Max-Age для кэширования preflight. Особая форма OPTIONS * относится ко всему серверу, а не к конкретному ресурсу.
С какими ограничениями HTTP приходилось сталкиваться?
Заголовок раздела «С какими ограничениями HTTP приходилось сталкиваться?»Коротко. Это вопрос про личный опыт: ждут конкретных историй, а не теории. Каркас ответа — назвать ограничение, объяснить, как оно проявилось в вашей системе, и что вы с ним сделали.
Глубже. Готовый набор ограничений, из которых можно собрать честный ответ. Лимиты на размер: заголовки ограничены сервером (в Go MaxHeaderBytes, по умолчанию 1 МБ; в nginx large_client_header_buffers), длина URL на практике ограничена прокси (порядка 8 КБ) — отсюда переход с GET с гигантским query на POST с телом. Отсутствие пуша от сервера: HTTP запрос-ответ, поэтому для реалтайма нужны SSE, WebSocket или long polling. HOL blocking в HTTP/1.1 и лимит в 6 соединений на хост в браузере. Stateless: приходится тащить сессию в cookie/токен, что усложняет отзыв прав. Отсутствие встроенных дедлайнов и ретраев — всё руками, и без сквозного контекста бюджеты не соблюдаются. Промежуточные прокси, которые буферизуют ответ и ломают стриминг (в nginx лечится proxy_buffering off и X-Accel-Buffering: no) или обрывают idle-соединения раньше, чем клиентский пул (классическая гонка с http: server closed idle connection в Go — лечится IdleConnTimeout меньше, чем у балансировщика). Плюс лимит cookie в 4 КБ и сложности с CORS.
Что такое REST API?
Заголовок раздела «Что такое REST API?»Коротко. REST — архитектурный стиль (Рой Филдинг, 2000), а REST API — интерфейс, построенный по его ограничениям: клиент-сервер, отсутствие состояния сессии на сервере, кэшируемость, единообразный интерфейс (ресурсы адресуются URI, манипулируются через стандартные методы, представления самоописываемы), слоистость и опционально code-on-demand.
Глубже. Единообразие интерфейса — то, что чаще всего понимают неверно. Оно означает: ресурс — существительное во множественном числе (/orders/42/items), действие выражается методом, а не глаголом в URL (POST /orders вместо /createOrder), результат — стандартный код статуса, представление помечено Content-Type. Шестое ограничение — HATEOAS (ответ содержит ссылки на доступные переходы) — в реальных API почти не встречается, и по модели зрелости Ричардсона большинство «REST API» остаются на уровне 2. Про stateless стоит сказать точно: сервер не хранит состояние клиентской сессии между запросами, а состояние ресурсов, разумеется, хранит. Плюсы стиля: кэшируемость на любом уровне (браузер, CDN, прокси), простое горизонтальное масштабирование, предсказуемость для сторонних разработчиков. Минусы: over-/under-fetching (лечится частично fields-параметрами или GraphQL), N+1 запросов на связанные ресурсы, слабая типизация контракта (лечится OpenAPI).
Что такое HTTP?
Заголовок раздела «Что такое HTTP?»Коротко. См. выше про HTTP-протокол: прикладной stateless-протокол «запрос-ответ» поверх TCP/QUIC, определяющий методы, коды статуса, заголовки и тело.
Глубже. Если вопрос задают повторно или в конце блока, обычно хотят услышать не определение, а картину целиком в двух предложениях: «HTTP задаёт семантику — что мы просим и что получили; версии протокола задают только то, как эти сообщения кодируются на проводе; всё, что выглядит как состояние, приложение добавляет само поверх cookies и токенов».
Каким образом HTTP запрос доходит от клиента до сервера? Описать весь флоу.
Заголовок раздела «Каким образом HTTP запрос доходит от клиента до сервера? Описать весь флоу.»Коротко. Разбор URL → поиск в кэшах DNS (браузер, ОС, резолвер) и, при промахе, рекурсивный запрос к DNS до получения IP → ARP/маршрутизация до нужного узла → TCP-хендшейк с сервером (или с балансировщиком) → TLS-хендшейк с проверкой сертификата и ALPN → отправка HTTP-запроса → на стороне сервера: балансировщик/reverse proxy → приложение → БД → формирование ответа → ответ проходит обратно по тому же соединению → клиент читает статус, заголовки, тело.
Глубже. Детали, которые превращают ответ в «сеньорский». На сетевом уровне: пакет уходит через default gateway, MAC-адрес шлюза узнаётся по ARP, дальше маршрутизация по BGP, по пути возможен NAT (порт источника переписывается) и MTU/фрагментация. На уровне балансировки: L4-балансировщик просто проксирует TCP, L7 (nginx, Envoy) терминирует TLS и разбирает HTTP, добавляя X-Forwarded-For/Forwarded — в Go это важно, потому что r.RemoteAddr покажет адрес прокси, а не клиента. На стороне Go-сервера: net.Listener.Accept отдаёт соединение, http.Server запускает горутину на соединение, читает и парсит запрос, находит хендлер в ServeMux, вызывает цепочку middleware. Ответ пишется в bufio.Writer поверх сокета, WriteHeader фиксирует статус, после чего заголовки менять поздно. При keep-alive соединение возвращается в пул, и следующий запрос стартует сразу с шага отправки.
В чем отличие 80 и 443 портов, принимающих клиентские запросы?
Заголовок раздела «В чем отличие 80 и 443 портов, принимающих клиентские запросы?»Коротко. 80 — порт HTTP по умолчанию (открытый текст), 443 — HTTPS (HTTP внутри TLS). Технически это просто номера портов из соглашения IANA; принципиальная разница в том, что на 443 перед HTTP выполняется TLS-хендшейк.
Глубже. Практические следствия. На 80-м обычно оставляют только редирект на HTTPS (301 на https:// + HSTS) и обработку ACME-challenge для выпуска сертификатов. На 443 в TLS-хендшейке через SNI виден запрашиваемый хост, что позволяет одному IP обслуживать много доменов с разными сертификатами, а через ALPN согласуется h2 или http/1.1. Ничто не мешает запустить HTTPS на 8443, а HTTP на 8080 — тогда порт придётся указывать в URL явно. В Linux порты ниже 1024 привилегированные: процессу нужен root или capability CAP_NET_BIND_SERVICE, поэтому Go-сервисы в контейнерах слушают 8080/8443, а маппинг на 80/443 делает оркестратор.
Чем отличается HTTP2 от HTTP1?
Заголовок раздела «Чем отличается HTTP2 от HTTP1?»Коротко. См. выше про HTTP/2: бинарный фрейминг вместо текста, мультиплексирование множества потоков в одном TCP-соединении вместо одного запроса за раз, сжатие заголовков HPACK, приоритеты потоков; семантика методов и кодов не изменилась.
Глубже. Если этот вопрос идёт после общего про HTTP/2, добавьте эксплуатационную сторону: HTTP/2 меняет профиль нагрузки на сервер — вместо шести соединений от клиента приходит одно с сотней потоков, поэтому важны настройки MaxConcurrentStreams и защита от Rapid Reset (CVE-2023-44487, когда клиент открывает и мгновенно сбрасывает потоки, заставляя сервер бесконечно создавать обработчики; в Go это закрыто в 1.21.x и уточнено настройками в 1.24). Также HTTP/2 плохо сочетается с L4-балансировкой: одно долгоживущее соединение прилипает к одному бэкенду.
За счет чего осуществляется стриминг данных в HTTP1? Какая нужна инструкция для этого?
Заголовок раздела «За счет чего осуществляется стриминг данных в HTTP1? Какая нужна инструкция для этого?»Коротко. За счёт Transfer-Encoding: chunked — тело передаётся кусками, каждый предваряется своей длиной в hex, конец обозначается нулевым чанком. Это позволяет начать отдачу, не зная итогового размера, то есть не выставляя Content-Length.
Глубже. На проводе это выглядит так:
HTTP/1.1 200 OKContent-Type: text/event-streamTransfer-Encoding: chunked
1a\r\ndata: first message\n\n\r\n0\r\n\r\nВ Go net/http включает chunked автоматически, если хендлер не выставил Content-Length и пишет больше буфера; чтобы данные ушли клиенту немедленно, нужен flush:
func stream(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-store") rc := http.NewResponseController(w) // Go 1.20+ for i := 0; i < 10; i++ { select { case <-r.Context().Done(): return default: } fmt.Fprintf(w, "data: tick %d\n\n", i) if err := rc.Flush(); err != nil { return } time.Sleep(time.Second) }}Подводные камни: WriteTimeout у сервера обрубит долгий стрим — снимайте дедлайн через rc.SetWriteDeadline(time.Time{}); промежуточный nginx по умолчанию буферизует ответ и стрим «залипает» — нужен proxy_buffering off или заголовок X-Accel-Buffering: no; сжатие gzip тоже буферизует. В HTTP/2 Transfer-Encoding запрещён — там потоковость обеспечивается самими DATA-фреймами, но код на Flusher работает одинаково. Альтернативный способ «стриминга» в HTTP/1 — диапазонные запросы Range/206 Partial Content, но это скачивание по частям по инициативе клиента, а не push.
Какие знаешь заголовки в HTTP и что они означают?
Заголовок раздела «Какие знаешь заголовки в HTTP и что они означают?»Коротко. Основные группы: адресация и согласование контента (Host, Accept, Accept-Encoding, Accept-Language, Content-Type, Content-Length, Content-Encoding), аутентификация (Authorization, WWW-Authenticate, Cookie, Set-Cookie), кэширование (Cache-Control, ETag, If-None-Match, Last-Modified, If-Modified-Since, Expires, Vary, Age), управление соединением (Connection, Keep-Alive, Transfer-Encoding, Upgrade), редиректы и метаданные (Location, Retry-After, Date, Server, User-Agent, Referer), CORS (Origin, Access-Control-*) и безопасность (Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options).
Глубже. Заголовки, о которых стоит уметь говорить подробнее. Vary указывает, по каким заголовкам запроса различается ответ, — без Vary: Accept-Encoding CDN может отдать gzip-ответ клиенту, не поддерживающему сжатие. ETag + If-None-Match дают условные запросы и 304 Not Modified без тела — самый дешёвый способ ревалидации. Cache-Control различает no-cache (кэшировать можно, но каждый раз ревалидируй) и no-store (не сохранять вообще) — это классическая путаница. X-Forwarded-For/Forwarded несут исходный IP клиента за прокси и доверять им можно только от своих прокси. Content-Type c charset важен для текстовых типов; X-Content-Type-Options: nosniff запрещает браузеру угадывать тип и защищает от MIME-sniffing-атак. В Go имена заголовков канонизируются (http.CanonicalHeaderKey), поэтому r.Header.Get("content-type") работает, а прямой доступ к мапе r.Header["content-type"] — нет.
Перед отправкой/обработкой запроса логировать URL и метод;
Заголовок раздела «Перед отправкой/обработкой запроса логировать URL и метод;»Коротко. Это требование из тестового задания к логирующему middleware, а не вопрос: перед вызовом основного обработчика (или перед отправкой исходящего запроса) надо записать в лог метод и URL.
Глубже. На сервере это делается обёрткой http.Handler, на клиенте — собственным http.RoundTripper:
func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { slog.InfoContext(r.Context(), "request", "method", r.Method, "url", r.URL.String()) next.ServeHTTP(w, r) })}Практические оговорки: не логируйте полный URL как есть, если в query бывают токены или персональные данные — вырезайте чувствительные параметры; добавляйте корреляционный идентификатор (X-Request-Id) и кладите его в контекст, чтобы связать записи; для клиента используйте RoundTripper, а не обёртку над каждым вызовом, — тогда логи покроют и ретраи, и редиректы.
После получения/отправки ответа логировать статус код и тело ответа.
Заголовок раздела «После получения/отправки ответа логировать статус код и тело ответа.»Коротко. Тоже пункт задания. Сложность в том, что http.ResponseWriter не позволяет прочитать записанные статус и тело, поэтому его надо обернуть своей структурой, перехватывающей WriteHeader и Write.
Глубже. Рабочая обёртка и её использование:
type capturingWriter struct { http.ResponseWriter status int body bytes.Buffer}
func (c *capturingWriter) WriteHeader(code int) { c.status = code c.ResponseWriter.WriteHeader(code)}
func (c *capturingWriter) Write(b []byte) (int, error) { if c.status == 0 { c.status = http.StatusOK // неявный 200 при первой записи } c.body.Write(b) // в проде ограничьте объём! return c.ResponseWriter.Write(b)}
func LogResponse(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cw := &capturingWriter{ResponseWriter: w} start := time.Now() next.ServeHTTP(cw, r) slog.InfoContext(r.Context(), "response", "status", cw.status, "bytes", cw.body.Len(), "duration", time.Since(start), ) })}Что важно проговорить: полное тело логировать в проде нельзя — это и утечка персональных данных, и рост объёма логов, и лишняя аллокация; ограничивайте размер (первые N килобайт), маскируйте чувствительные поля и включайте тело только для ошибок или по семплированию. Вторая тонкость: обёртка «теряет» дополнительные интерфейсы http.Flusher, http.Hijacker, io.ReaderFrom — с Go 1.20+ это частично решается через http.ResponseController, который умеет доставать нужные возможности из вложенного писателя (обёртка должна реализовать метод Unwrap() http.ResponseWriter). На клиентской стороне тело ответа надо читать через io.TeeReader или буферизовать и подменять resp.Body, иначе основной код получит уже вычитанный поток.
Есть два сервиса. Как они могут взаимодействовать? ( Sync (grpc/rest) / async (brokers) )
Заголовок раздела «Есть два сервиса. Как они могут взаимодействовать? ( Sync (grpc/rest) / async (brokers) )»Коротко. Двумя способами: синхронно — вызов «запрос-ответ» через REST/HTTP или gRPC, когда вызывающий ждёт результат; асинхронно — через брокер сообщений (Kafka, RabbitMQ, NATS) или очередь, когда отправитель публикует событие и не ждёт ответа. Выбор определяется тем, нужен ли результат немедленно и допустимо ли временное отставание.
Глубже. Синхронный вызов прост и даёт немедленную консистентность, но связывает доступность: если B лежит, A тоже деградирует, и цепочка вызовов перемножает вероятности отказа — отсюда таймауты, ретраи, circuit breaker, bulkhead. Асинхронный обмен развязывает сервисы во времени: брокер сглаживает пики, потребитель может лежать и догнать позже; платой становится eventual consistency, необходимость идемпотентных обработчиков (доставка at-least-once), контроль порядка сообщений и сложность отладки. Есть промежуточные варианты: синхронный приём с немедленным 202 Accepted и фоновой обработкой; паттерн transactional outbox, чтобы запись в БД и публикация события были атомарными; sagas для распределённых бизнес-транзакций. Практическое правило: команды, требующие ответа пользователю здесь и сейчас, — синхронно; уведомления, интеграции, расчёты, рассылки, репликация данных между сервисами — асинхронно.
Что происходит после открытия любого сайта в браузерере? (dns, http или https запрос по строчкам, ответ, его статус, рендеринг html).
Заголовок раздела «Что происходит после открытия любого сайта в браузерере? (dns, http или https запрос по строчкам, ответ, его статус, рендеринг html).»Коротко. Браузер разбирает URL, проверяет HSTS-список и кэш, резолвит домен через DNS, устанавливает TCP-соединение и TLS-сессию, отправляет GET / HTTP/1.1 с заголовками Host, User-Agent, Accept, получает ответ со статусом (обычно 200 OK и Content-Type: text/html), парсит HTML в DOM, параллельно догружает CSS, JS и картинки, строит CSSOM и дерево рендера, выполняет layout и paint, исполняет JS и показывает страницу.
Глубже. Ключевые подробности по этапам. DNS-резолв идёт по цепочке кэшей: браузер → ОС → резолвер провайдера → корневые/TLD/авторитативные серверы. Перед подключением браузер проверяет HSTS preload — если домен там есть, http:// мгновенно превращается в https:// без запроса. Соединение: TCP-хендшейк, затем TLS с SNI и ALPN, возможно сразу HTTP/2 или переход на HTTP/3 по ранее полученному Alt-Svc. Запрос выглядит буквально как GET / HTTP/1.1, Host: example.com, Accept: text/html,..., Accept-Encoding: gzip, br, Cookie: .... Ответ может быть не 200: 301/302 — редирект по Location и всё начинается заново; 304 — берём из кэша; 4xx/5xx — страница ошибки. Рендеринг: HTML-парсер строит DOM инкрементально, синхронный <script> без async/defer блокирует парсинг, CSS блокирует рендеринг (render-blocking), затем происходит layout (расчёт геометрии), paint и composite. После загрузки срабатывают события DOMContentLoaded и load. Отдельно упомяните preconnect/preload, приоритеты загрузки ресурсов в HTTP/2 и то, что критический путь рендеринга — это то, ради чего меряют метрики LCP/FCP.
Сравни gRPC и REST;
Заголовок раздела «Сравни gRPC и REST;»Коротко. См. выше про разницу REST и gRPC. Сжато: REST — ресурсы, HTTP-методы, JSON, человекочитаемость, кэшируемость, работа из браузера; gRPC — методы сервиса, protobuf, HTTP/2, кодогенерация, стриминг, меньше трафика и латентности, но нужен toolchain и прокси для браузера.
Глубже. Табличное сравнение по осям, которое удобно проговорить вслух: контракт — OpenAPI (описание, часто пишется отдельно и рассинхронизируется) против .proto (источник правды, из которого генерится код); формат — текстовый JSON против бинарного protobuf; транспорт — любой HTTP против HTTP/2 обязательно; модель вызова — CRUD над ресурсами против произвольных методов и четырёх видов стриминга; ошибки — коды HTTP против кодов gRPC в трейлере; кэширование — стандартное HTTP-кэширование против «своими руками»; отладка — curl/браузер против grpcurl/reflection; эволюция схемы — конвенции и версии в URL против правил совместимости protobuf с номерами полей.
С restfull api работал?
Заголовок раздела «С restfull api работал?»Коротко. Вопрос про личный опыт, ответ «да» без деталей ничего не даёт. Каркас: назвать конкретный сервис и его домен, перечислить, какие решения принимали (структура ресурсов, версионирование, пагинация, обработка ошибок, аутентификация), и упомянуть один нетривиальный случай с объяснением выбора.
Глубже. Что интервьюер надеется услышать: как вы версионируете API (префикс /v1 в пути, заголовок, или через media type — и почему); как устроена пагинация (offset против keyset и почему на больших таблицах offset деградирует); как оформлены ошибки (единый формат, RFC 9457, коды); как сделана идемпотентность для повторных POST; как документируется контракт (OpenAPI, генерация клиентов, contract-тесты); как тестируется (httptest.Server, таблицы тестов, golden files). Типичная ошибка — рассказ уровня «делали CRUD на gin, роуты, хендлеры» без единого проектного решения.
Чем rest api отличает от rpc?
Заголовок раздела «Чем rest api отличает от rpc?»Коротко. RPC ориентирован на действия: клиент вызывает удалённую процедуру (createOrder, cancelOrder), URL и транспорт — деталь реализации. REST ориентирован на ресурсы: сущность адресуется URI, а действие выражается стандартным HTTP-методом (POST /orders, DELETE /orders/42), плюс REST опирается на HTTP-семантику — коды статуса, кэширование, идемпотентность.
Глубже. Практические следствия. В RPC естественно выражаются операции, которые не ложатся на CRUD («пересчитать прогноз», «отправить письмо») — в REST их приходится либо моделировать как ресурс-действие (POST /orders/42/cancellations), либо честно признавать RPC-эндпоинт. RPC обычно даёт более строгий контракт и кодогенерацию, REST — универсальность и инфраструктурную поддержку (кэши, прокси, браузеры, стандартные инструменты). Идемпотентность в REST выражена методом, в RPC — только договорённостью. И честная ремарка: большинство «REST API» на практике — это RPC с ресурсными URL, уровень 2 по модели зрелости Ричардсона; называть это ошибкой не стоит, но понимать разницу нужно.
В rest и grpc в каком виде данные передаются?
Заголовок раздела «В rest и grpc в каком виде данные передаются?»Коротко. В REST — как правило текстом: JSON (реже XML, form-urlencoded, multipart для файлов), в теле HTTP-сообщения, с указанием Content-Type. В gRPC — бинарно: protobuf-сообщения, упакованные в length-prefixed-кадры (1 байт флага сжатия + 4 байта длины big-endian + сериализованное сообщение) внутри DATA-фреймов HTTP/2, с Content-Type: application/grpc+proto.
Глубже. Почему protobuf компактнее: в JSON каждое поле несёт своё имя строкой и все числа записаны текстом, а protobuf кодирует поле как «номер + тип» в одном varint-теге и использует varint для целых, поэтому маленькие числа занимают 1 байт; поля со значением по умолчанию в proto3 вообще не передаются. Обратная сторона — сообщение нечитаемо без схемы, и разобрать его в логах или в tcpdump без .proto нельзя. gRPC поддерживает и другие кодеки (application/grpc+json), а сжатие настраивается отдельно (grpc-encoding: gzip) — в отличие от REST, где сжатие тела делает сам HTTP через Content-Encoding. Ещё разница: метаданные в gRPC (аналог заголовков) едут в HEADERS-фреймах, а бинарные значения требуют суффикса -bin в имени ключа и base64-кодирования.
Для чего нужен протокол HTTP?
Заголовок раздела «Для чего нужен протокол HTTP?»Коротко. HTTP — прикладной протокол запрос-ответ для передачи представлений ресурсов между клиентом и сервером: клиент указывает URI и метод, сервер отвечает кодом статуса, заголовками и телом. Он stateless, расширяем через заголовки и служит универсальным транспортом и для веб-страниц, и для API, и для межсервисного взаимодействия.
Глубже. Практическая ценность HTTP не в «передаче данных» — это умеет и голый TCP, — а в наборе согласованных соглашений, которые понимают все промежуточные узлы. Из-за того что метод и заголовки стандартизованы, CDN знает, что GET можно закэшировать, а POST — нет; балансировщик знает, что запрос можно безопасно переотправить на другой бэкенд, если он идемпотентен; браузер знает, что при 401 надо показать форму, а при 301 — переписать закладку. Плюс HTTP согласует представление (Accept, Content-Type, Accept-Encoding), управляет условными запросами (ETag/If-None-Match, Last-Modified/If-Modified-Since), диапазонами (Range), аутентификацией (Authorization, WWW-Authenticate) и временем жизни кэша (Cache-Control). Отдельный бонус — вездесущность: 443/tcp открыт в любом корпоративном периметре, поэтому поверх HTTP строят и WebSocket (через Upgrade), и gRPC (поверх HTTP/2), и long-polling, и SSE.
Как обеспечивается безопасность в HTTPS?
Заголовок раздела «Как обеспечивается безопасность в HTTPS?»Коротко. HTTPS — это HTTP, установленный поверх TLS-сессии. TLS даёт конфиденциальность (симметричное AEAD-шифрование), целостность (тег аутентификации и счётчики последовательности, защищающие от подмены и повтора) и аутентификацию сервера (сертификат X.509, проверенный по цепочке до доверенного корня, плюс сверка имени хоста с SAN). Ключи вырабатываются эфемерным ECDH, поэтому есть forward secrecy: компрометация приватного ключа сервера не расшифровывает записанный ранее трафик.
Глубже. В TLS 1.3 (RFC 8446) хендшейк занимает 1-RTT: клиент в ClientHello сразу присылает key_share, сервер отвечает ServerHello со своей долей, дальше всё, включая сертификат, уже зашифровано. Набор шифров сведён к AEAD (AES-GCM, ChaCha20-Poly1305), выкинуты RSA key transport, CBC, сжатие и ренеготиация — то есть целые классы атак (BEAST, CRIME, POODLE, Logjam) закрыты на уровне дизайна. Сессии возобновляются через PSK-тикеты, есть 0-RTT — но 0-RTT данные подвержены replay, поэтому в них допустимы только идемпотентные запросы. Аутентификация сервера — не задача самого TLS-рукопожатия, а задача приложения: проверить, что сертификат валиден по времени, подписан доверенным CA и его SAN совпадает с хостом из URL. В Go это делает crypto/tls автоматически, и главный антипаттерн на ревью — tls.Config{InsecureSkipVerify: true}, который убивает аутентификацию и превращает HTTPS в шифрование «до кого-то». Вокруг базового TLS строятся HSTS (Strict-Transport-Security, чтобы не было downgrade на http://), Certificate Transparency, OCSP stapling, а для взаимной проверки — mTLS (tls.Config{ClientAuth: tls.RequireAndVerifyClientCert, ClientCAs: pool}). Из свежего в Go: с 1.24 по умолчанию включён гибридный постквантовый обмен ключами X25519MLKEM768, а в crypto/tls появилась клиентская поддержка Encrypted Client Hello, который прячет SNI. Важно помнить и границы: TLS не скрывает IP-адрес и объём трафика, а SNI без ECH виден в открытом виде, поэтому «HTTPS = полная анонимность» — неверно.
Что такое OpenAPI, REST, Swagger?
Заголовок раздела «Что такое OpenAPI, REST, Swagger?»Коротко. REST — архитектурный стиль (диссертация Роя Филдинга, 2000) с набором ограничений: клиент-сервер, stateless, кэшируемость, единообразный интерфейс, слоистость и опциональный code-on-demand. OpenAPI — формальная спецификация для описания HTTP API в YAML/JSON. Swagger — историческое имя этой же спецификации (версия 2.0) и сегодня — набор инструментов SmartBear вокруг неё: Swagger UI, Swagger Editor, Swagger Codegen.
Глубже. Путаница возникает потому, что в 2015 году спецификация Swagger 2.0 была передана в Linux Foundation и переименована в OpenAPI Specification; версии 3.0.x и 3.1 — это уже «OpenAPI», а слово «Swagger» корректно применять только к инструментам. OpenAPI 3.1 важен тем, что стал полностью совместим с JSON Schema 2020-12 — до этого в 3.0 был свой диалект с расхождениями (nullable, отсутствие examples в схемах и т. п.). Про REST на собесе полезно упомянуть модель зрелости Ричардсона: уровень 0 — один endpoint в стиле RPC поверх POST; уровень 1 — появились ресурсы; уровень 2 — используются HTTP-методы и коды по назначению (здесь находится 99% «REST API» в индустрии); уровень 3 — HATEOAS, когда ответ содержит ссылки на доступные переходы. Формально REST без HATEOAS — не REST, и честный ответ звучит так: «то, что мы обычно называем REST, — это уровень 2, RPC поверх HTTP с ресурсной адресацией». В Go экосистема такая: генерация серверных стабов и клиентов из спеки — oapi-codegen и ogen; обратный подход (код → спека) — go-swagger или аннотации swaggo/swag; валидация запросов по спеке в рантайме — kin-openapi.
Как решать проблемы backward compatibility openAPI?
Заголовок раздела «Как решать проблемы backward compatibility openAPI?»Коротко. Правило одно: изменения должны быть аддитивными. Добавлять необязательные поля в запрос и новые поля в ответ можно, удалять или переименовывать поля, делать необязательное обязательным, сужать типы и убирать значения из enum запроса — нельзя. Ломающие изменения проводят через новую версию API или через приём expand-contract, а факт наличия ломающего изменения проверяют в CI автоматическим диффом спеки.
Глубже. Полезно держать в голове асимметрию направлений: у запроса «расширение» ломает клиентов (новое обязательное поле — старый клиент его не пришлёт), а у ответа «расширение» ломает клиентов только если они читают строго; наоборот, добавление нового значения в enum ответа — ломающее изменение для сгенерированных клиентов с закрытыми enum, хотя интуитивно кажется безобидным. Отсюда практики: писать «толерантных читателей» (Postel’s law — игнорировать неизвестные поля), не переиспользовать одну схему одновременно для запроса и ответа, не делать additionalProperties: false без нужды, а enum в ответах либо документировать как открытые, либо использовать строку с валидацией на сервере. Процедура expand-contract (parallel change) выглядит так: сначала добавляем новое поле рядом со старым и заполняем оба, потом мигрируем потребителей, затем помечаем старое deprecated: true и отдаём заголовок Sunset с датой, и только после истечения срока удаляем. Версионирование чаще всего делают в пути (/v1/..., /v2/...) — это грубо, но прозрачно для роутинга и логов; альтернативы — версия в media type (Accept: application/vnd.acme.v2+json) или в заголовке, они элегантнее, но неудобны для кэширования и отладки. Инструментально всё это закрывается диффом спеки в pipeline: oasdiff умеет отдельно классифицировать breaking changes и валить сборку, есть также openapi-diff и Optic. И самое надёжное — контрактные тесты со стороны потребителей (consumer-driven contracts, Pact), потому что спека может остаться совместимой, а поведение поменяться.
Какие REST-овые методы знаете? В чем между ними разница? Можно ли сделать так, что GET будет делать DELETE, а POST будет делать PUT?
Заголовок раздела «Какие REST-овые методы знаете? В чем между ними разница? Можно ли сделать так, что GET будет делать DELETE, а POST будет делать PUT?»Коротко. Основные методы: GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE, CONNECT. Различаются они по трём свойствам из RFC 9110: безопасность (не меняет состояние: GET, HEAD, OPTIONS, TRACE), идемпотентность (повтор даёт тот же эффект: безопасные + PUT и DELETE) и кэшируемость (по умолчанию GET и HEAD). Технически заставить GET удалять ресурс можно — сервер волен обработать любой метод как угодно, — но это грубое нарушение семантики, и инфраструктура сломает вам жизнь.
Глубже. Разница между PUT и POST: PUT — полная замена ресурса по известному клиенту URI, идемпотентен (два одинаковых PUT дают одно состояние); POST — «обработай это по своим правилам», обычно создание подчинённого ресурса, URI назначает сервер и возвращает в Location с кодом 201, не идемпотентен. PATCH — частичное изменение, в общем случае не идемпотентен (например, {"op":"add"} в JSON Patch), формат тела задаётся отдельно: application/merge-patch+json (RFC 7386) или application/json-patch+json (RFC 6902). HEAD — тот же GET без тела, для проверки существования и метаданных. OPTIONS — узнать поддерживаемые методы, в браузерах — preflight CORS. TRACE и CONNECT в API практически не встречаются, CONNECT используется прокси для туннелирования TLS.
Теперь про «GET, который удаляет». Ничто в HTTP не мешает написать такой обработчик, но: браузеры и антивирусы префетчат ссылки, поисковые краулеры ходят по ним, CDN и прокси кэшируют ответы GET и могут вернуть закэшированный результат вместо повторного удаления, а клиенты и балансировщики автоматически ретраят идемпотентные запросы при сетевых ошибках. Классическая история — Google Web Accelerator, который «удалил» кучу данных в админках, где действия висели на GET-ссылках. В Go это тоже осязаемо: http.Transport может молча переотправить запрос, если соединение из пула оказалось разорвано, и делает это для запросов, которые считает повторяемыми (без тела либо с заданным GetBody) — при неидемпотентной семантике на GET получите двойное применение эффекта. Симметрично «POST, который делает PUT» — менее опасно (POST не кэшируется и не ретраится), и это ровно то, что делают легаси-клиенты через X-HTTP-Method-Override: PUT, когда прокси режет нестандартные методы; это признанный костыль, но именно костыль. Правильный ответ на собесе: технически возможно, семантически недопустимо, потому что HTTP-методы — это контракт не только с вашим клиентом, но и со всей промежуточной инфраструктурой.
Однако, после перезапуска источник начнет с прошлой “подтвержденной” позиции, задаваемой cookie. Поэтому каждое значение cookie, которое вернул вызов Next, после сохранения данных в приемнике, должно быть фиксировано вызовом Commit, причем строго в той же последовательности, в которой их вернул Next
Заголовок раздела «Однако, после перезапуска источник начнет с прошлой “подтвержденной” позиции, задаваемой cookie. Поэтому каждое значение cookie, которое вернул вызов Next, после сохранения данных в приемнике, должно быть фиксировано вызовом Commit, причем строго в той же последовательности, в которой их вернул Next»Обрывок исходника, вопрос не восстанавливается. Это фрагмент описания контракта курсорного источника данных (at-least-once доставка с подтверждением позиции), а не вопрос по HTTP; слово «cookie» здесь означает непрозрачный токен позиции, а не HTTP-cookie.
Как работает httptest?
Заголовок раздела «Как работает httptest?»Коротко. net/http/httptest даёт два независимых инструмента: httptest.NewRecorder() — фейковый http.ResponseWriter, который просто записывает статус, заголовки и тело в память, чтобы вызвать хендлер напрямую без сети; и httptest.NewServer(handler) — настоящий HTTP-сервер на 127.0.0.1 со случайным портом, к которому ходят реальным клиентом. Плюс httptest.NewRequest для сборки серверного *http.Request без парсинга сети.
Глубже. ResponseRecorder — это структура с полями Code, HeaderMap и Body *bytes.Buffer; метод Result() возвращает *http.Response со снимком заголовков, сделанным в момент WriteHeader, поэтому заголовки, выставленные после записи тела, в Result() не попадут — это самая частая ловушка. Recorder не реализует http.Hijacker, не делает chunked-кодирования, не применяет Content-Length и не проверяет корректность порядка вызовов, то есть тестирует хендлер, а не HTTP-стек. httptest.NewRequest(method, target, body) собирает запрос в «серверном» виде: заполняет RemoteAddr значением 192.0.2.1:1234, ставит Host: example.com, если в target только путь, и паникует на некорректном target — в Go 1.24 добавили вариант NewRequestWithContext. httptest.NewServer поднимает реальный net.Listener, отдаёт srv.URL, а srv.Close() дожидается завершения активных запросов (поэтому дедлок при блокирующем хендлере — типичная причина зависшего теста). Для TLS есть NewTLSServer с самоподписанным сертификатом: ходить туда нужно клиентом srv.Client(), который уже доверяет этому сертификату, либо добавить srv.Certificate() в свой пул. Для HTTP/2 берут httptest.NewUnstartedServer, выставляют srv.EnableHTTP2 = true и вызывают srv.StartTLS(). Выбор между двумя подходами простой: Recorder — быстрый юнит-тест одного хендлера и middleware-цепочки; NewServer — интеграционный тест, когда важны реальные редиректы, cookie-jar, таймауты, TLS или вы тестируете свой HTTP-клиент (тогда сервер выступает заглушкой чужого API).
package api
import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing")
func handler(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") // паттерны ServeMux с wildcard — Go 1.22+ w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(map[string]string{"id": id})}
func TestHandler_Recorder(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("GET /items/{id}", handler)
req := httptest.NewRequest(http.MethodGet, "/items/42", nil) rec := httptest.NewRecorder()
mux.ServeHTTP(rec, req)
res := rec.Result() defer res.Body.Close() if res.StatusCode != http.StatusOK { t.Fatalf("got %d, want 200", res.StatusCode) } body, _ := io.ReadAll(res.Body) if !strings.Contains(string(body), `"42"`) { t.Fatalf("unexpected body: %s", body) }}
// Тот же пакет: интеграционный вариант через реальный сервер.func TestClient_AgainstServer(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTeapot) })) defer srv.Close()
resp, err := srv.Client().Get(srv.URL + "/any") if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusTeapot { t.Fatalf("got %d, want 418", resp.StatusCode) }}Верно ли утверждение, что HTTPS - это всего лишь HTTP поверх TLS?
Заголовок раздела «Верно ли утверждение, что HTTPS - это всего лишь HTTP поверх TLS?»Коротко. В первом приближении да: семантика HTTP не меняется, меняется только то, что байты идут через TLS-сессию. Но «всего лишь» — упрощение: HTTPS определён как отдельная URI-схема со своим портом 443, своими требованиями к проверке подлинности сервера и своим origin в браузерной модели безопасности, а в HTTP/3 никакого «TLS поверх TCP» вообще нет — там TLS 1.3 встроен в QUIC.
Глубже. Что именно добавляется сверх «просто TLS». Во-первых, схема https:// обязывает клиента проверить сертификат и сверить имя хоста — сам по себе TLS позволяет и анонимные шифронаборы, и произвольные сертификаты, требование валидации приходит из спецификации HTTPS (исторически RFC 2818, сейчас RFC 9110 §4.2.2). Во-вторых, меняется порядок установления соединения: через прокси клиент сначала делает CONNECT host:443 и только потом внутри туннеля начинает TLS. В-третьих, версия HTTP теперь выбирается на этапе TLS через расширение ALPN (h2, http/1.1), а не через Upgrade; HTTP/2 в браузерах существует только по TLS и требует TLS 1.2+ с ограниченным списком шифров. В-четвёртых, для браузера http://example.com и https://example.com — разные origin: разные cookie-политики (Secure, __Host- префикс), разный доступ к «мощным» API (geolocation, service worker), блокировка mixed content. Наконец, промежуточные кэши перестают видеть содержимое, поэтому кэширование переезжает на терминатор TLS. Отдельный нюанс для Go: в стандартной библиотеке HTTP/2 включается автоматически при TLS, а с Go 1.24 появились поля Protocols у http.Server и http.Transport, которыми можно явно перечислить HTTP1, HTTP2 и UnencryptedHTTP2 (h2c). Так что формулировка «HTTPS = HTTP over TLS» — хорошая первая фраза, но за ней должны следовать оговорки про схему, ALPN, origin и HTTP/3.
Какие версии есть у HTTP и какие между ними отличия?
Заголовок раздела «Какие версии есть у HTTP и какие между ними отличия?»Коротко. HTTP/0.9 (только GET, без заголовков), HTTP/1.0 (заголовки, коды статусов, соединение на запрос), HTTP/1.1 (обязательный Host, keep-alive по умолчанию, chunked, кэширование, Range), HTTP/2 (бинарные фреймы, мультиплексирование потоков в одном TCP-соединении, HPACK-сжатие заголовков), HTTP/3 (то же самое поверх QUIC/UDP с встроенным TLS 1.3). Семантика методов и кодов во всех версиях начиная с 1.1 одна и та же — различается только формат передачи по проводу.
Глубже. HTTP/1.1 решал проблему стоимости TCP-соединений через persistent connections, но остался с head-of-line blocking на уровне протокола: в одном соединении ответы идут строго по порядку, pipelining на практике не взлетел из-за багов прокси. Обходили это шестью параллельными соединениями на хост, доменным шардингом и склейкой ресурсов. HTTP/2 (RFC 9113) переводит всё в бинарные фреймы (HEADERS, DATA, SETTINGS, WINDOW_UPDATE, RST_STREAM), вводит независимые потоки внутри одного соединения, псевдозаголовки :method, :path, :scheme, :authority вместо стартовой строки, обязательный нижний регистр имён заголовков, HPACK со статической и динамической таблицами и покадровый flow control. Протокольный HOL-блокинг он снимает, но транспортный остаётся: потеря одного TCP-сегмента тормозит все потоки сразу, поэтому при потерях HTTP/2 может проигрывать HTTP/1.1. Server Push формально есть, но по факту умер — Chrome убрал его поддержку, вместо него используют 103 Early Hints. HTTP/3 (RFC 9114) решает транспортный HOL: QUIC поверх UDP даёт независимые от потерь стримы, объединённый хендшейк (TLS 1.3 встроен, соединение поднимается за 1-RTT, при возобновлении — 0-RTT), миграцию соединения по Connection ID при смене сети, и QPACK вместо HPACK — чтобы сжатие заголовков не создавало собственную зависимость по порядку. Обнаружение обычно через заголовок Alt-Svc или DNS-запись HTTPS/SVCB. Со стороны Go: net/http из коробки поддерживает HTTP/1.1 и HTTP/2 (реализация в golang.org/x/net/http2, встраиваемая в стандартную библиотеку); HTTP/3 в стандартной библиотеке на Go 1.24 нет — используют сторонний quic-go, при этом в стандартной библиотеке уже появился экспериментальный пакет crypto/tls с поддержкой QUIC-интерфейсов и net/quic во внутренней разработке. Отдельный практический момент: h2c (HTTP/2 без TLS) в Go до 1.24 требовал обёртки h2c.NewHandler из x/net, с 1.24 включается через Protocols с флагом UnencryptedHTTP2.
Частые ошибки на собесе
Заголовок раздела «Частые ошибки на собесе»- Говорят «HTTPS — это другой протокол». Нет, это тот же HTTP внутри TLS; отличается транспортная обёртка, порт и схема, а семантика идентична.
- Путают
401и403:401— «не знаю, кто ты» (нуженWWW-Authenticate),403— «знаю, но нельзя». - Называют HTTP/2 «быстрее, потому что бинарный». Основной выигрыш — мультиплексирование и HPACK, а не бинарность как таковая; и HOL blocking на уровне TCP в HTTP/2 остаётся.
- Утверждают, что HTTP/3 — это «HTTP поверх UDP». Точнее: поверх QUIC, который сам даёт надёжность, порядок внутри потока и встроенный TLS 1.3, а UDP — лишь способ доставки датаграмм.
- Считают, что
PUTиPATCHвзаимозаменяемы.PUTзаменяет ресурс целиком и идемпотентен,PATCHменяет частично и в общем случае нет. - Забывают, что в Go у
http.Clientиhttp.Serverтаймауты по умолчанию нулевые, то есть бесконечные, и что тело ответа надо и закрывать, и дочитывать, иначе соединение не вернётся в пул. - Говорят «cookie хранятся на сервере». Cookie хранит клиент; сервер хранит только связанные данные сессии, если он вообще их хранит.
- Отдают
200 OKс полемerrorв теле — это ломает клиентов, кэши и мониторинг; ошибка должна быть выражена кодом статуса. - Сравнивают gRPC и HTTP как равнозначные альтернативы, не оговорив, что gRPC работает поверх HTTP/2.
- Говорят «HTTP/2 добавил новые методы/коды» — нет, семантика (методы, статусы, заголовки) общая, менялся только транспортный синтаксис.
- Считают, что HTTP/3 «просто HTTP/2 по UDP»: главное отличие — устранение транспортного head-of-line blocking и встроенный в QUIC TLS 1.3, а не сам факт UDP.
- Путают безопасность и идемпотентность: DELETE не безопасен (меняет состояние), но идемпотентен; POST не идемпотентен, а PUT — да.
- Утверждают, что «REST — это JSON по HTTP». REST — стиль с ограничениями; формат тела в нём вообще не зафиксирован, а большинство реальных API — уровень 2 по Ричардсону, без HATEOAS.
- Называют Swagger и OpenAPI синонимами без оговорок: Swagger 2.0 — старое имя спецификации, сейчас это набор инструментов, а спецификация называется OpenAPI (3.0/3.1).
- Считают добавление нового значения в enum ответа безопасным изменением — для строго сгенерированных клиентов это breaking change.
- Думают, что
InsecureSkipVerify: true«просто отключает предупреждение»: он выключает аутентификацию сервера целиком и делает MITM тривиальным. - Тестируют хендлер через
httptest.NewRecorderи удивляются, что заголовки, выставленные послеWriteHeader, не видны вResult(). - Считают, что TLS скрывает всё: IP, размеры и тайминги пакетов, а без ECH и SNI остаются наблюдаемыми.
Что почитать
Заголовок раздела «Что почитать»- RFC 9110 «HTTP Semantics» — методы, коды, заголовки: https://www.rfc-editor.org/rfc/rfc9110.html
- RFC 9112 (HTTP/1.1), RFC 9113 (HTTP/2), RFC 9114 (HTTP/3) — форматы сообщений по версиям.
- MDN Web Docs, раздел HTTP — самый практичный справочник по заголовкам, кодам и CORS: https://developer.mozilla.org/en-US/docs/Web/HTTP
- Документация пакета
net/httpи исходникиtransport.go/server.go: https://pkg.go.dev/net/http - RFC 6265bis (Cookies) и OWASP Cheat Sheet по сессиям: https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- gRPC over HTTP/2 — описание протокола на проводе: https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md
- RFC 9110 «HTTP Semantics» — методы, коды, заголовки, свойства безопасности и идемпотентности: https://www.rfc-editor.org/rfc/rfc9110
- RFC 9113 (HTTP/2) и RFC 9114 (HTTP/3) — форматы фреймов, HPACK/QPACK, отличия транспорта: https://www.rfc-editor.org/rfc/rfc9113
- RFC 8446 «TLS 1.3» — хендшейк, 0-RTT, forward secrecy: https://www.rfc-editor.org/rfc/rfc8446
- Документация пакета
net/http/httptest: https://pkg.go.dev/net/http/httptest - OpenAPI Specification 3.1 и инструменты диффа спеки: https://spec.openapis.org/oas/latest.html , https://github.com/oasdiff/oasdiff
Список исходных вопросов с привязкой к компаниям: ../questions/http.md