Рассмотрим функцию, которая отправляет GET-запрос на URL и возвращает код состояния HTTP. При использовании HTTP/1 (GODEBUG=http2client=0) вызов этой функции 100 раз к index.golang.org занимает около 710 миллисекунд на Go 1.26 на машине автора. На Go 1.27 та же программа выполняется примерно за 150 миллисекунд, то есть почти в 5 раз быстрее.
Разрыв возникает из-за стоимости установления соединений. Запрос по новому соединению требует TCP-рукопожатия (1 время прохождения туда и обратно, или RTT), TLS-рукопожатия (2 RTT при TLS 1.2), а затем самого HTTP-запроса, что в сумме составляет около 3 RTT. В межрегиональных коммуникациях односторонние задержки часто достигают или превышают 50 мс, поэтому одна только настройка может занять около 300 мс. Даже с index.golang.org, который использует TLS 1.3 и имеет относительно низкую задержку, открытие нового соединения остается очень заметным.
Чтобы избежать этих затрат, HTTP-клиенты и серверы стараются повторно использовать соединения. Клиенты и серверы HTTP/1.0 включают это с помощью заголовка Connection: keep-alive, а соединения HTTP/1.1 по умолчанию постоянные. HTTP/2 мультиплексирует несколько запросов и ответов в одном TCP-соединении.
Однако в HTTP/1 повторное использование может не сработать. Ответы в одном соединении отправляются последовательно, поэтому net/http может найти начало следующего ответа только после того, как тело текущего ответа будет прочитано до конца. В примере тело ответа никогда не читается, поэтому при вызове resp.Body.Close net/http обнаруживает непрочитанное тело и не может безопасно повторно использовать соединение. До Go 1.26 это приводило к созданию нового соединения для каждого запроса.
Источник: Hacker News · Сводку подготовил HeadlinesBriefing