Skip to content

Оптимизация производительности

HTTPC изначально спроектирован высокопроизводительным: переиспользование пула соединений, мультиплексирование HTTP/2, пулинг объектов, единственное выделение под объект результата. В большинстве сценариев отличный результат дают пресеты конфигурации напрямую; для дальнейшей настройки нужно понимать внутренние механизмы, чтобы действовать прицельно.

Сравнение пресетов конфигурации

HTTPC предлагает 5 пресетов конфигурации, каждый системно настроен под свой сценарий. Ниже по категориям приведены точные значения ключевых полей — для удобства выбора.

Конфигурация таймаутов

ПолеDefaultSecurePerformanceTestingMinimal
TimeoutConfig.Request180s15s60s180s180s
TimeoutConfig.Dial10s5s15s5s5s
TimeoutConfig.TLSHandshake10s5s15s5s5s
TimeoutConfig.ResponseHeader0 (отключено)10s0 (отключено)0 (отключено)0 (отключено)
TimeoutConfig.IdleConn90s30s120s30s30s

Конфигурация соединений

ПолеDefaultSecurePerformanceTestingMinimal
MaxIdleConns50201001010
MaxConnsPerHost1052052
EnableHTTP2ВключёнВключёнВключёнОтключёнВключён
EnableCookiesОтключеныОтключеныВключеныВключеныОтключены
EnableDoHОтключенОтключенОтключенОтключенОтключен

Конфигурация безопасности

ПолеDefaultSecurePerformanceTestingMinimal
MaxResponseBodySize10MB5MB50MB10MB1MB
MaxDecompressedBodySize100MB100MB100MB100MB100MB
ValidateURLВключенаВключенаВключенаОтключенаВключена
ValidateHeadersВключенаВключенаВключенаОтключенаВключена
StrictContentLengthВключёнВключёнОтключёнВключёнВключён
AllowPrivateIPsfalsefalsefalsetruefalse
InsecureSkipVerifyfalsefalsefalsetruefalse

Конфигурация повторов

ПолеDefaultSecurePerformanceTestingMinimal
MaxRetries31310
Delay1s2s500ms100ms0
BackoffFactor2.02.01.52.01.0
MaxRetryDelay30s30s30s30s30s
EnableJitterВключёнВключёнВключёнОтключёнОтключён

Значения запросов по умолчанию

ПолеDefaultSecurePerformanceTestingMinimal
FollowRedirectsВключеныОтключеныВключеныВключеныОтключены
MaxRedirects1010101010
UserAgenthttpc/1.0httpc/1.0httpc/1.0httpc-test/1.0httpc/1.0

TestingConfig запрещён в продакшене

TestingConfig() отключает проверку URL/заголовков, верификацию TLS-сертификатов и защиту от SSRF — только локальная разработка и тесты. При вызове в нетестовом окружении выводится предупреждение безопасности. В продакшене используйте SecureConfig() или DefaultConfig().

Выбор пресета по сценарию

СценарийРекомендуемый пресетКорректировки
Универсальный веб-сервисDefault
Работа с URL от пользователейSecure
Внутренние микросервисы, высокая конкурентностьPerformanceУвеличить MaxIdleConns по числу бэкендов
Одноразовые скриптыMinimal
Сервис скачивания файловPerformanceУвеличить MaxResponseBodySize
API для финансов/медициныSecure + кастомизацияДобавить middleware аудита
Локальная разработка/юнит-тестыTestingНи в коем случае не deployить в продакшен
go
// Высокая пропускная способность — пресет напрямую
client, _ := httpc.New(httpc.PerformanceConfig())

// Точечная подстройка отдельных полей поверх пресета
cfg := httpc.PerformanceConfig()
cfg.Timeouts.Request = 120 * time.Second
cfg.Connection.MaxIdleConns = 200
client, _ := httpc.New(cfg)

Модель конкурентности: один Client обслуживает все goroutine

Client и DomainClient HTTPC конкурентно-безопасны — любые методы можно вызывать одновременно из нескольких goroutine; в библиотеке есть специальные интеграционные тесты конкурентности (internal/concurrency), покрывающие публичный API под высокой нагрузкой. Поэтому правильный паттерн конкурентности предельно прост:

На глобальном/сервисном уровне создаётся 1 Client

        ├── goroutine 1 ──┐
        ├── goroutine 2 ──┼── совместно используют один пул соединений и пул объектов
        └── goroutine N ──┘

Связь конкурентной ёмкости и пула соединений:

СценарийПоведение
Конкурентность ≤ MaxConnsPerHost (HTTP/1.1)Каждый запрос занимает своё соединение, без очередей
Конкурентность > MaxConnsPerHost (HTTP/1.1)Лишние запросы ждут в очереди транспортного уровня на свободное соединение (не ошибка, но задержка растёт)
HTTP/2 включён (по умолчанию)Запросы к одному хосту делят одно соединение с мультиплексированием, MaxConnsPerHost редко становится узким местом

Два способа ограничить конкурентность

  • Со стороны клиента: поднять Connection.MaxConnsPerHost до ≥ пиковой конкурентности (для HTTP/1.1);
  • Со стороны вызовов: ограничить конкурентность семафором на буферизованном channel (полный пример ниже), активно защищая нижестоящий сервис. Часто их комбинируют: семафор ограничивает поток по выносливости нижестоящего сервиса, пул соединений подстраивается под лимит семафора.
go
package main

import (
	"fmt"
	"log"
	"net/http"
	"net/http/httptest"
	"sync"
	"sync/atomic"
	"time"

	"github.com/cybergodev/httpc"
)

func main() {
	// Локальный имитационный сервер: каждый запрос стабильно занимает 50ms
	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		time.Sleep(50 * time.Millisecond)
		w.WriteHeader(http.StatusOK)
	}))
	defer server.Close()

	cfg := httpc.DefaultConfig()
	cfg.Security.AllowPrivateIPs = true // Разрешаем подключение к локальному тестовому серверу 127.0.0.1
	client, err := httpc.New(cfg)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	const (
		total       = 20
		maxInFlight = 5 // Семафор: не более 5 запросов в полёте одновременно
	)

	sem := make(chan struct{}, maxInFlight)
	var wg sync.WaitGroup
	var okCount int64
	start := time.Now()

	for i := 0; i < total; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			sem <- struct{}{}                // Захват семафора
			defer func() { <-sem }()         // Освобождение семафора

			result, err := client.Get(server.URL)
			if err != nil {
				return
			}
			if result.IsSuccess() {
				atomic.AddInt64(&okCount, 1)
			}
		}()
	}
	wg.Wait()

	fmt.Printf("%d/%d успешно, затрачено %v (последовательно потребовалось бы %v)\n",
		okCount, total, time.Since(start), total*50*time.Millisecond)
	// Пример вывода: 20/20 успешно, затрачено ~250ms (последовательно ~1s) — 5-сторонняя конкурентность даёт ~5-кратную пропускную способность
}

При массовом опросе большого числа независимых URL удобен и другой паттерн — worker pool: фиксированное число worker-goroutine вычитывает задания из jobs channel, естественно ограничивая конкурентность числом worker, семафор не нужен. Полная реализация — в Продвинутых примерах.

Принципы настройки пула соединений

Пул соединений — ядро производительности HTTP-клиента. Пул HTTPC построен на http.Transport стандартной библиотеки Go, но поверх него добавлены автоматические вычисления и безопасные значения по умолчанию.

Автоматический подсчёт простаивающих соединений

MaxIdleConnsPerHost (лимит простаивающих соединений на хост) вручную задавать не нужно — HTTPC выводит его из MaxConnsPerHost:

Простаивающие соединения = MaxConnsPerHost / 2, ограничено диапазоном [2, 10]

Конкретные правила (calculateIdleConnsPerHost):

MaxConnsPerHostАвтоматический лимит простаивающихПояснение
0 (без лимита)10Значение по умолчанию — верхняя граница
11Сначала берётся нижняя граница 2, затем «не больше максимума соединений» опускает до 1
22Ровно нижняя граница
52Половина округляется к нижней границе
105Пресет Default
2010Пресет Performance, верхняя граница
10010Сверх верхней границы берётся 10

Почему именно MaxConnsPerHost / 2

Простаивающие соединения — это «кэш соединений»: уже установленные, но временно не используемые. Значение, равное половине максимума, балансирует между «переиспользованием существующего соединения» (попадание в кэш) и «созданием нового» (при промахе кэша нужно новое рукопожатие) и не даёт простаивающим соединениям избыточно занимать ресурсы серверной стороны.

TCP Keep-Alive

Пул HTTPC использует фиксированный интервал TCP keep-alive в 30 секунд (defaultKeepAlive = 30 * time.Second). После установки соединения операционная система периодически отправляет keep-alive пробы, детектируя мёртвые соединения. Таймаут IdleConn управляет временем жизни простаивающего соединения в пуле (в Default — 90s); оба механизма работают в связке.

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

func main() {
    // Микросервисы с высоким QPS: увеличиваем пул
    cfg := httpc.PerformanceConfig()
    cfg.Connection.MaxIdleConns = 200   // Глобальный лимит простаивающих соединений
    cfg.Connection.MaxConnsPerHost = 50 // Максимум соединений на хост (простаивающие автоматически — 10)
    cfg.Timeouts.IdleConn = 300 * time.Second // Простаивающие соединения живут дольше, выше переиспользование

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Запросы горячего пути напрямую переиспользуют соединения из пула
    for i := 0; i < 100; i++ {
        result, err := client.Get("https://api.example.com/data")
        if err != nil {
            log.Printf("Запрос %d не удался: %v", i, err)
            continue
        }
        fmt.Printf("Запрос %d: %d\n", i, result.StatusCode())
    }
}

Преимущества HTTP/2 для производительности

HTTP/2 включён по умолчанию (EnableHTTP2 = true) и даёт три крупных улучшения:

ВозможностьHTTP/1.1HTTP/2
МультиплексированиеКаждое соединение — один запросНесколько запросов делят одно соединение
Сжатие заголовковПовторная отправка открытым текстомСжатие заголовков HPACK
Переиспользование соединенийKeep-alive, последовательноеПараллельные потоки (stream)

HTTP/2 и пул соединений

Мультиплексирование HTTP/2 позволяет одному TCP-соединению одновременно переносить несколько запросов, радикально сокращая накладные расходы на установку соединений. При высокой конкурентности обращений к одному хосту пропускная способность HTTP/2 многократно превышает HTTP/1.1. Откат к HTTP/1.1 происходит только с TestingConfig() (явно отключает HTTP/2) либо когда соединение не поддерживает согласование ALPN.

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

func main() {
    // HTTP/2 уже включён в конфигурации по умолчанию
    cfg := httpc.DefaultConfig()
    cfg.Connection.EnableHTTP2 = true // И так true, но явное объявление нагляднее

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Конкурентные запросы к сайту с поддержкой HTTP/2 (большинство CDN/облаков)
    // Переиспользуется одно TCP-соединение, новое на каждый запрос не создаётся
    start := time.Now()
    for i := 0; i < 10; i++ {
        result, err := client.Get("https://http2.golang.org/")
        if err != nil {
            log.Printf("Запрос %d не удался: %v", i, err)
            continue
        }
        // Proto() возвращает версию протокола, например "HTTP/2.0"
        fmt.Printf("Запрос %d: %s, код состояния %d\n", i, result.Proto(), result.StatusCode())
    }
    fmt.Printf("10 запросов заняли: %v\n", time.Since(start))
}

Механизмы оптимизации памяти

HTTPC многоуровнево оптимизирует работу с памятью; основная идея — меньше выделений в куче и переиспользование объектов.

Единичное выделение resultBundle

Возвращаемый на каждый запрос *Result несёт три вложенные структуры: RequestInfo (информация запроса), ResponseInfo (информация ответа), RequestMeta (метаданные вроде длительности). Традиционный подход требует отдельных выделений под Result и три вложенные структуры — четырёх выделений в куче. HTTPC упаковывает их в один resultBundle, и одного выделения хватает на всё:

Традиционно: 4 независимых выделения (Result + RequestInfo + ResponseInfo + RequestMeta)
HTTPC: 1 выделение (resultBundle), три указателя Result смотрят в одну область памяти

Вызывающий получает *Result, чьи поля Request, Response, Meta (указатели) ссылаются на соответствующие структуры внутри bundle — полная прозрачность. Поскольку вызывающий может долго держать *Result, здесь не подходит пул объектов (пулирование привело бы к гонкам за данные) — утилизацию выполняет GC.

Пул объектов движка

Слой движка HTTPC широко применяет sync.Pool для переиспользования короткоживущих объектов, снижая нагрузку на GC:

Пулируемый объектНазначениеОписание
engine.ResponseОбъект ответаПосле завершения запроса возвращается в пул, переиспользуется следующим запросом
engine.RequestОбъект запросаАналогично
strings.BuilderПостроение строкСборка URL, форматирование ошибок, сериализация Config
http.HeaderMap заголовков HTTPОбработка заголовков запроса/ответа
bytes.BufferКодирование JSON/multipartПредвыделение по исходной ёмкости
time.TimerТаймеры повторовИзбегает частого создания таймеров
gzip/flate readerРаспаковкаПереиспользование декомпрессоров

Разделение труда между пулом объектов и resultBundle

Внутренние объекты движка (Response/Request/Builder) живут недолго и проходят цикл borrow-return внутри запроса — им подходит пулирование. Возвращаемый вызывающему *Result имеет неопределённое время жизни — ему подходит единичное выделение и утилизация GC. Механизмы дополняют друг друга, каждый на своём месте.

Горячий путь с малым числом выделений

Помимо пула объектов, горячий путь запроса содержит набор прицельных оптимизаций против выделений:

Точка оптимизацииМеханизм
Пакетное выделение при глубоком копировании заголовковCloneHeader сначала считает все значения, затем одним выделением создаёт общий базовый массив — «по одному выделению на заголовок (N штук)» превращается в 1
Экранирование параметров запроса без выделенийСтроки, не требующие экранирования, возвращаются как есть (ноль выделений); при необходимости экранирования байты пишутся в буфер из пула
Прямая запись числовых параметровint/float64/bool и другие числа пишутся в построитель через strconv.Append*, без промежуточных строк
Перенос владения заголовками запросаОбычный путь запроса и путь скачивания переносят владение header map движкового Response в Result, а не клонируют его
Инлайн-массив цепочки перенаправленийПервые 8 перенаправлений хранятся в инлайн-массиве фиксированной длины пулируемого объекта, срез-переполнение выделяется только после 8 — подавляющее большинство запросов не выделяет ничего под цепочку
Переиспользование таймеров сна повторовtime.Timer для отката повторов пулируется и переиспользуется; при частых повторах таймеры не создаются заново
Защита ёмкости пулаОбъекты сверх порога не возвращаются в пул (например, header map > 64 записей, ёмкость query builder > 4096) — большие объекты не залёживаются в пуле и не раздувают память

Внутренние метрики и здоровье

Движок собирает пер-запросные метрики чисто атомарными операциями (без блокировок): общее число запросов, успехи/неудачи и сглаженную задержку по формуле скользящего среднего новое среднее = (старое среднее×9 + текущая задержка) / 10; ошибки ниже 10% считаются здоровым состоянием. Эти метрики обслуживают собственные решения движка о здоровье и не являются публичным API — метрики запросов на уровне приложения собирайте через MetricsMiddleware (см. middleware): он вызывает колбэк с методом/URL/кодом состояния/длительностью и напрямую стыкуется с Prometheus и другими системами мониторинга.

Об этом можно не заботиться

Все перечисленные оптимизации прозрачны для вызывающего. Просто используйте API как обычно — переиспользование соединений, пулирование объектов и единичные выделения происходят внутри автоматически:

go
package main

import (
    "fmt"
    "log"

    "github.com/cybergodev/httpc"
)

func main() {
    client, err := httpc.NewDefault()
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    // Result создаётся заново на каждый запрос, утилизируется GC, ручное освобождение не требуется
    result, err := client.Get("https://api.example.com/data")
    if err != nil {
        log.Fatal(err)
    }

    // В горячем пути предпочитайте RawBody() вместо Body()
    // RawBody() возвращает исходный срез байт; Body() — заранее сохранённую строку; String() — отладочное форматирование (самое дорогое)
    data := result.RawBody()
    fmt.Printf("Размер ответа: %d байт\n", len(data))
    fmt.Printf("Длительность запроса: %v\n", result.Meta.Duration)
}

Примеры настройки под рабочие нагрузки

Бюджет таймаутов

Четыре транспортных таймаута (Dial, TLSHandshake, ResponseHeader и неявная передача тела) вместе ограничены общим бюджетом Timeouts.Request. Меняя пресет, сохраняйте иерархию «сумма частей ≤ общий бюджет» и не допускайте бессмысленных конфигураций вроде «таймаут установки соединения больше общего таймаута»:

Timeouts.Request (общий бюджет, по умолчанию 180s)
 ├── Timeouts.Dial           Установка соединения (по умолчанию 10s)
 ├── Timeouts.TLSHandshake   TLS-рукопожатие (по умолчанию 10s)
 ├── Timeouts.ResponseHeader Ожидание заголовков ответа (в Default/Performance 0 = без ограничения на транспортном уровне)
 └── Передача тела ответа     Доступно всё оставшееся время
Рабочая нагрузкаRequestDial/TLSПояснение
Внутренние микросервисы5–10s1–2sБыстрое падение, ошибку обрабатывают повторы/размыкатель выше по стеку
Публичные API30s5sБаланс между межсетевыми задержками и редкими медленными ответами
AI/долгие задачи300s+10sДлинное тело ответа занимает весь остаток бюджета
Скачивание больших файлов0 (управление через context)15sОбщее время контролирует ctx в Download, отдельный запрос — WithTimeout

Взаимодействие ResponseHeader и WithTimeout

Пресеты Default/Performance ставят ResponseHeader в 0 (не принудительно на транспортном уровне), отдавая WithTimeout() полный контроль над долгими ответами; пресет Secure ставит 10s как защиту от атак класса slowloris. Если вы вручную сужаете ResponseHeader, помните: он может обрезать медленный ответ раньше WithTimeout.

Долгие запросы к AI API

API ИИ-инференса могут отвечать минутами — таймауты нужно ослаблять:

go
// AI API может отвечать 5–15 минут — не обрезайте дефолтными 180s
result, err := httpc.Post("https://api.ai.example.com/v1/completions",
    httpc.WithJSON(payload),
    httpc.WithTimeout(900*time.Second), // 15 минут
)

Почему в Default ResponseHeader равен 0

TimeoutConfig.ResponseHeader = 0 означает отсутствие принудительного таймаута заголовков на транспортном уровне — временем управляет context-уровень (TimeoutConfig.Request или WithTimeout). Это гарантирует WithTimeout() полный контроль над запросами с долгими ответами. Нужна транспортная защита от slowloris — берите SecureConfig() (там 10s).

Высокий QPS в микросервисах

Частые вызовы между внутренними микросервисами требуют большого пула соединений:

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

func main() {
    cfg := httpc.PerformanceConfig()
    // Пул настраиваем по числу экземпляров бэкендов
    cfg.Connection.MaxIdleConns = 300   // Всего простаивающих соединений
    cfg.Connection.MaxConnsPerHost = 30 // На каждый экземпляр бэкенда
    // Ответы микросервисов обычно быстрые — сокращаем таймауты ради быстрого падения
    cfg.Timeouts.Request = 10 * time.Second
    cfg.Retry.Delay = 200 * time.Millisecond
    cfg.Retry.BackoffFactor = 2.0
    cfg.Retry.MaxRetries = 2

    client, err := httpc.New(cfg)
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    start := time.Now()
    // Частые запросы переиспользуют пул, TCP/TLS не пересоздаются
    for i := 0; i < 50; i++ {
        result, err := client.Get("http://user-service:8080/api/users")
        if err != nil {
            log.Printf("Запрос %d не удался: %v", i, err)
            continue
        }
        _ = result
    }
    fmt.Printf("50 запросов заняли: %v\n", time.Since(start))
}

Скачивание больших файлов (потоковое)

Для больших файлов используйте Download(): он автоматически включает потоковый режим, тело ответа идёт из сети напрямую на диск, потребление памяти не зависит от размера файла, поддерживаются докачка и контрольная сумма:

go
package main

import (
	"context"
	"log"

	"github.com/cybergodev/httpc"
)

func main() {
	cfg := httpc.PerformanceConfig()
	cfg.Security.MaxResponseBodySize = 500 * 1024 * 1024 // Лимит 500MB

	client, err := httpc.New(cfg)
	if err != nil {
		log.Fatal(err)
	}
	defer client.Close()

	dcfg := httpc.DefaultDownloadConfig()
	dcfg.FilePath = "/tmp/large-file.zip"
	dcfg.ResumeDownload = true // Докачка

	result, err := client.Download(
		context.Background(),
		"https://example.com/large-file.zip",
		dcfg,
	)
	if err != nil {
		log.Fatal(err)
	}
	log.Printf("Скачивание завершено: %d байт", result.BytesWritten)
}

Не включайте WithStreamBody для обычных методов запроса

WithStreamBody(true) имеет смысл только для путей вроде Download, напрямую потребляющих ответ движка. На Get/Post/Request и другие обычные методы он приведёт к тому, что тело ответа всё равно будет полностью прочитано в Result с закрытием нижележащего потока — у возвращённого Result тело пустое, и вызывающий поток не получит. Правильный вход для потребления больших тел ответа — Download (подробно в Загрузке и выгрузке файлов).

Скрапинг и пул прокси

Скраперы используют пул прокси для ротации IP; HTTPC автоматически поднимает число повторов, чтобы каждый прокси был опробован хотя бы раз (подробно в Повторных попытках и отказоустойчивости):

go
cfg := httpc.DefaultConfig()
cfg.Connection.ProxyPool = []string{
    "http://proxy1:8080",
    "http://proxy2:8080",
    "http://proxy3:8080",
    "http://proxy4:8080",
    "http://proxy5:8080",
}
cfg.Connection.ProxyRotateOnStatus = []int{403} // 403 запускает смену прокси
cfg.Connection.ProxyPoolStrategy = httpc.ProxyStrategyRoundRobin
// MaxRetries автоматически поднимается до 4 (число прокси − 1), чтобы все 5 прокси были опробованы

Антипаттерны производительности

АнтипаттернПричинаПравильно
Новый Client на каждый запросСоединения не переиспользуются, каждый раз заново TCP/TLS-рукопожатиеОдин глобальный экземпляр Client
Слишком большой MaxResponseBodySizeНенужное ослабление лимита памятиЗадавайте по фактическому размеру ответов
result.String() в горячем путиДополнительные расходы на построение строкиresult.Body() или result.RawBody()
Слишком маленький пул соединенийПри высокой конкурентности соединений не хватает, запросы ждут в очередиMaxConnsPerHost подстраивайте под конкурентность
WithStreamBody для обычных запросовУ возвращённого Result тело пустое, поток не получитьБольшие тела — через Download
Отключение HTTP/2Деградация до последовательных запросов HTTP/1.1Оставьте включённым по умолчанию
Игнорирование Close()Утечка соединенийdefer client.Close()
Забыли переиспользовать глобальный клиентПостоянное создание/уничтожение ClientСоздать один раз, держать долго
Лимитирование числом goroutineЛомает нижестоящий сервис, ловит 429/размыкательСемафор или worker pool на число запросов в полёте

Client обязательно переиспользовать

Основа производительности HTTP — переиспользование соединений. Новый Client на каждый запрос означает TCP-рукопожатие в три шага плюс TLS-рукопожатие каждый раз: задержка взлетает с долей миллисекунды до десятков миллисекунд. В микросервисах внедряйте Client как singleton в структуру сервиса — со временем жизни сервиса.

go
package main

import (
    "fmt"
    "log"
    "time"

    "github.com/cybergodev/httpc"
)

// Демонстрация антипаттерна: новый Client на каждый запрос
func main() {
    start := time.Now()

    for i := 0; i < 5; i++ {
        // ❌ Каждый оборот цикла создаёт Client — соединения не переиспользуются
        client, err := httpc.NewDefault()
        if err != nil {
            log.Fatal(err)
        }
        result, err := client.Get("https://httpbin.org/get")
        client.Close() // Закрываем каждый раз, пул соединений опустошается
        if err != nil {
            log.Printf("Запрос %d не удался: %v", i, err)
            continue
        }
        _ = result
    }
    // 5 запросов занимают куда больше времени, чем вариант с переиспользованием Client
    fmt.Printf("Время антипаттерна: %v\n", time.Since(start))

    // ✅ Правильно: переиспользуем Client
    client, err := httpc.NewDefault()
    if err != nil {
        log.Fatal(err)
    }
    defer client.Close()

    start = time.Now()
    for i := 0; i < 5; i++ {
        result, err := client.Get("https://httpbin.org/get")
        if err != nil {
            log.Printf("Запрос %d не удался: %v", i, err)
            continue
        }
        _ = result
    }
    fmt.Printf("Время с переиспользованием: %v\n", time.Since(start))
}

Что дальше