HTTP-клиент: timeout, Transport и connection pool
http.Client выполняет запросы, а его Transport (http.RoundTripper) устанавливает соединения, TLS, proxy и содержит пул idle connections. Нормальная практика — создать client/transport при старте и переиспользовать их; request-specific deadline приходит из context.Context.
Зачем это на интервью
Вызов внешнего API без deadline может навсегда занять goroutine и соединение. На E4 важно закрывать response body, проверять non-2xx status и разделять timeout всего обмена с настройками dial/TLS/header. На E5 нужны бюджет, retry только безопасных операций и capacity pool-а.
Минимум для E4
- Передавать
NewRequestWithContext, а не делать фоновый запрос из handler. - Настроить
Client.Timeoutкак верхнюю границу всего запроса либо гарантировать deadline в context. - Переиспользовать client; не изменять общий
Transportконкурентно после начала работы. - Закрывать
resp.Bodyсразу послеDo, ограничивать error body и проверятьStatusCode. - Не делать blind retry POST; учитывать идемпотентность,
Retry-Afterи контекст.
Углубление для E5/Senior
Client.Timeout включает dial, redirects и чтение body. Он удобен как fail-safe, но deadline верхнего запроса должен быть меньше или равен бюджету caller-а. Transport позволяет отдельно задать DialContext, TLSHandshakeTimeout, ResponseHeaderTimeout, ExpectContinueTimeout, IdleConnTimeout, MaxIdleConns, MaxIdleConnsPerHost, MaxConnsPerHost. Ограничение MaxConnsPerHost защищает downstream, но вызывает очередь в transport; наблюдайте latency и cancellation.
Повтор допустим, когда семантика безопасна: GET/HEAD обычно, PUT/DELETE при корректной идемпотентности, POST — только с idempotency key и контрактом сервера. Exponential backoff с jitter не должен жить дольше request deadline. Ответ 429/503 не одинаково означает retry: уважайте Retry-After и бизнес-SLO.
Ключевые понятия
package catalog
import (
"context"
"fmt"
"io"
"net"
"net/http"
"time"
)
var transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{Timeout: 2 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
ForceAttemptHTTP2: true,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 20,
MaxConnsPerHost: 50,
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 3 * time.Second,
ResponseHeaderTimeout: 2 * time.Second,
}
var client = &http.Client{Transport: transport, Timeout: 5 * time.Second}
func Fetch(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil { return nil, err }
resp, err := client.Do(req)
if err != nil { return nil, fmt.Errorf("catalog request: %w", err) }
defer resp.Body.Close()
const maxBody = 1 << 20
body, err := io.ReadAll(io.LimitReader(resp.Body, maxBody+1))
if err != nil { return nil, fmt.Errorf("read catalog response: %w", err) }
if len(body) > maxBody { return nil, fmt.Errorf("catalog response exceeds %d bytes", maxBody) }
if resp.StatusCode < 200 || resp.StatusCode > 299 { return nil, fmt.Errorf("catalog status %s: %q", resp.Status, body) }
return body, nil
}Для production error payload следует логировать/возвращать с redaction и разумным лимитом. Глобальные переменные в примере допустимы только как неизменяемые объекты; часто client внедряют через конструктор для тестов.
Типовые вопросы
Client.Timeoutи context deadline?- Первый — абсолютный лимит всей операции client-а; context выражает бюджет caller-а и отменяет все поддерживающие его зависимости. Обычно нужны оба, причём client — страховка.
- Почему обязательно
Closebody?- Иначе transport не освобождает связанные ресурсы; соединение не сможет переиспользоваться и возможна утечка FD.
- Нужно ли читать body перед Close?
- Для небольшой response обычно читают до EOF, чтобы connection мог вернуться в pool; при большой/ненужной response закрывают, принимая возможное закрытие соединения.
- Когда нужен отдельный
Transport?- При отличающейся политике proxy/TLS/pool/лимитов.
http.DefaultTransportнельзя мутировать: его могут использовать другие клиенты.
- При отличающейся политике proxy/TLS/pool/лимитов.
- Что означает
MaxConnsPerHost?- Предел активных, dialing и idle соединений на host; запросы сверх него ожидают, пока освободится connection или отменится context.
Практика
- Напишите client для
httptest.Server. Критерий готовности: тест проверяет method/header, response non-2xx возвращает ошибку с status, body закрывается. - Смоделируйте сервер, не отдающий headers. Критерий готовности:
ResponseHeaderTimeoutили context прекращает запрос в ожидаемый срок безtime.Sleepв production-коде. - Добавьте retry GET с jitter. Критерий готовности: не больше заданного числа попыток, нет retry после отмены context, POST без idempotency key не повторяется.
Частые ошибки и ловушки
- Создавать
http.DefaultClient-подобный client без timeout для внешнего вызова. - Делать
io.ReadAllответа без лимита, особенно для error path. - Клонировать
Transportпосле начала работы или менять его поля concurrent-но. - Retry всех сетевых ошибок и удваивать side effect.
- Отменять child context, но забывать вызвать
cancelпри раннем выходе.
Связанные темы
Практический Go · HTTP-сервер · HTTP и Web