Go 1.27 ускорил HTTP/1 почти в 5 раз
В блоге Go вышел разбор изменения, которое тихо ускоряет HTTP-клиенты на Go. Начиная с версии 1.27 пакет net/http сам дочитывает непрочитанное тело HTTP/1 ответа, и соединение уходит обратно в пул вместо того, чтобы закрыться.
Авторы показывают эффект на простой функции: она делает http.Get, забирает код статуса и закрывает тело через defer resp.Body.Close(), ничего из него не читая. Сто вызовов против index.golang.org по HTTP/1 на Go 1.26 занимают около 710 мс, на Go 1.27 около 150 мс. В коде не поменялось ни строчки.
Откуда берутся лишние сотни миллисекунд
Новое HTTPS-соединение начинается с TCP-рукопожатия (один RTT) и TLS-рукопожатия (еще два RTT для TLS 1.2). Между регионами RTT легко доходит до 100 мс, так что одна только установка соединения съедает около 300 мс, а сам запрос часто обрабатывается быстрее. Поэтому клиенты стараются держать соединения открытыми и гонять по ним запросы друг за другом.
В HTTP/1 ответы идут по сокету строго последовательно. Чтобы найти начало следующего ответа, текущее тело нужно прочитать до последнего байта. До Go 1.26 включительно Close() на непрочитанном теле означал, что соединение испорчено, и net/http открывал новое со всеми рукопожатиями. В HTTP/2 этой проблемы нет, там потоки мультиплексируются.
Где это стреляет на практике
Чаще всего так: клиенту нужен только статус, а API возвращает JSON вроде {"success": true}. Тело никто не читает, соединения выбрасываются одно за другим. Есть и вариант хитрее. encoding/json.Decoder останавливается на закрывающей скобке, и если сервер добавил в конце перевод строки, этот один байт остается в сокете и хоронит рабочее соединение.
Годами это лечили самописными функциями вроде drainAndCloseBody: вычитать тело через io.Copy в io.Discard, ограничить объем 256 КБ, добавить таймаут 50 мс и унести все в горутину, чтобы Close не блокировал код. Заодно появилась привычка сливать тела и в HTTP/2, где это бессмысленно.
Что сделали в Go 1.27
Теперь net/http.Transport делает это сам: асинхронно дочитывает тело HTTP/1 ответа, максимум 256 КБ и максимум 50 мс. Если сервер прислал Connection: close, слив пропускается. Команда Go советует просто закрывать тело, когда оно больше не нужно, а самописные обертки после перехода на 1.27 можно удалить.
Подробный разбор с примерами кода и эволюцией ручного решения: Go 1.27 ускорил HTTP/1 почти в 5 раз на uproger.