世界を動かす技術を、日本語で。

Go 1.27におけるHTTP/1接続の再利用の改善

2026年10月15日原文(go.dev)

概要

  • Go 1.27 以降、HTTP/1 通信のパフォーマンスが大幅に向上
  • 接続再利用 に関する仕様変更が主な要因
  • 未読のレスポンスボディ 処理の自動化
  • 開発者は 単純なコード で十分
  • net/httpパッケージの進化による恩恵

Go 1.27でHTTP/1通信が5倍速くなった理由

  • Go 1.26 では、 http.Get を100回呼ぶと約710ミリ秒かかった
  • Go 1.27 では同じコードが約150ミリ秒で完了、約5倍の高速化
  • 主な要因は接続再利用の改善 によるもの

接続確立のコスト

  • HTTP/1.x/2は基本的に TCP ・ TLS 上で動作
  • 新規接続時には TCPハンドシェイク(1RTT)、 TLSハンドシェイク(1〜2RTT) が必要
  • 地理的な距離がある場合、 1RTT=100ms 程度かかる場合も
  • 接続確立のラウンドトリップ が全体の多くの時間を占める

接続再利用の仕組み

  • HTTP/1.0: Connection: keep-alive ヘッダーで再利用
  • HTTP/1.1: デフォルトでコネクション再利用
  • HTTP/2: 1コネクションで複数リクエストを多重化
  • 再利用できれば 接続確立のコストを回避 できる

再利用が失敗するケース

  • HTTP/1 では、レスポンスボディを完全に読み切らないと再利用不可
  • 例えば defer resp.Body.Close() だけでは未読のボディが残る
  • net/httpは未読ボディがあると コネクションを破棄 し、新たに接続を作り直す
  • HTTP/2 ではこの問題は発生しない(多重化のため)

具体的な問題例

  • ステータスコードだけ取得し、ボディを読まないパターン
  • 一部だけボディを読む(例: encoding/json.DecoderでJSON末尾の改行を残す)
  • どちらも コネクション再利用失敗 につながる

手動での解決策とその限界

  • ボディを全て読み捨ててからClose することで再利用可能
    • 例: io.Copy(io.Discard, resp.Body); resp.Body.Close()
  • レスポンスが大きい場合は、 途中までしか読み捨てない 工夫も必要
    • 例: io.CopyN(io.Discard, resp.Body, 256<<10)
  • 遅いサーバー対策 として、読み捨てにタイムアウトも設定
  • これらを 非同期で処理 し、次のリクエストと並行させる工夫も

Go 1.27以降の自動化と推奨実装

  • Go 1.27からは net/http.Transportが自動で未読ボディを非同期でドレイン
    • 最大 50msまたは256KB まで
    • Connection: close ヘッダーがある場合は自動ドレインしない最適化も
  • 開発者は単純にresp.Body.Close()するだけで十分
  • HTTP/2の場合はドレイン不要 (多重化のため)

まとめとベストプラクティス

  • Go 1.27以降は シンプルなHTTPクライアント実装 で十分
  • 未読ボディのドレインやタイムアウト処理は net/httpが自動対応
  • 複雑なワークアラウンドは不要
  • Goの哲学である「 シンプルさ」を体現する進化

参考実装例(Go 1.27以降推奨):

func getStatus(url string) (int, error) {
    resp, err := http.Get(url)
    if err != nil {
        return 0, err
    }
    defer resp.Body.Close()
    return resp.StatusCode, nil
}
  • 上記のような シンプルなコード で高パフォーマンスを享受可能

Hackerたちの意見

やっと xhttp.DrainResponseBody を引退させられる!最近知ったけど、あんまり役に立ってなかったしね。

└

r60からプロジェクトや会社をまたいでコピーしてきた似たようなヘルパーをやっと削除できる!

これによって内部の動作が変わるのは残念だね。以前はレスポンスボディを閉じることでHTTPボディのダウンロードを完全にスキップできたのに。でも、ここでのトレードオフは好きだな。ダウンロード時間が50msに制限されるから、ネットワークがキャンセルされたHTTPボディに追いつけなくても、ちょっとしたトラフィックを待つだけで済むし。

最後の方で「Connection: close のボディは排出しない」と書いてあるけど、これってリバースプロキシでもうまくいくのかな?サーバーがボディを消費しないときにnginxでIOエラーが出て、クライアントに5xxコードが返ってくる問題があったんだ。FirefoxでもXHRのステータスコードが0になるのを見たことがあるけど、Chromeやcurlではボディを消費せずに早く閉じるのは期待通りに動くみたい。

何か見落としてるのかな?それともこの情報はnet/http自体にドキュメント化されてないの?この「Close」の実装は驚きだし、どこかに文書化されるべきだと思う。GoをHTTPクライアントとして使おうとしている人たちの間でよくある質問だから、ちょっと気持ち悪い感じはするけど、どこから来ているのかは理解できる。ただ、接続を強制的に閉じたい場合、再利用するつもりのないソケットを無駄に排出せずにどうすればいいのかが分からない。レスポンスオブジェクトに「Connection: close」ヘッダーを偽装するの?Body.Responseの具体的な型の下にあるソケットを掘り出してこのコードをバイパスできるのを期待してるの?それもできる方法があるはずだよね。

└

https://pkg.go.dev/net/http#Client.Do > ただし、Transportはボディが閉じられたときに、保守的な制限まで非同期的にレスポンスボディをEOFまで読み取ろうとします。似たような表現が他のいくつかの場所にも出てくるので、「非同期」で検索してみて。

└

Closeに直接文書化されていないのは妥当だと思う。Closeはこの問題に対しては高レベルすぎるかもしれない。例えば、HTTP/2では、リクエストライフサイクルが基盤のソケットから完全に切り離されているから、意図した通りには動かない。もし本当に接続をすぐに閉じたかったら、基盤の接続をつかんで直接閉じる方が論理的に見えるけど、これは基盤の層を知っておく必要があるから、ちょっと気持ち悪いよね。私にとってこの「Close」はHTTPリクエスト自体の視点からのもので、もし混乱していなければ、実際には接続を閉じていないと思う(例えば、ボディがなくて接続がキープアライブされている場合、基盤の接続は閉じないよね?)。

└

CloseNow()エラーメソッドは意味があると思う。

└

HTTPライブラリは一般的に厳しいね。実際のHTTPは少なくともDNSやTLS、場合によってはL4ネットワーキングの理解が必要だから。アプリケーション開発者をターゲットにしようとすると、漏れのある抽象化(HTTPバージョンすら知らないのにH/1ボディの排出動作が発生する)や、アプリ開発者が使い方を知らないほど低レベルなライブラリになっちゃう。rustのhyperやreqwestはその分割を試みた例だね。

jsonのDecoderを使うと、ボディを読み取っても再利用されない接続になることがあるよ。レスポンスに改行が含まれているとね。DecoderはXバイト(512だったかな?忘れた)しか読み取らなくて、そのバイト数にちょうど収まる構造体だと改行が残っちゃう。デコード後には必ずDecoder.Moreをチェックしてるよ(これでホワイトスペースが排出される)。

└

おお、それは全く知らなかったエッジケースだね。どこにドキュメントがあるのか分かりにくいけど、あの2つのパッケージの統合だからね。

└

読まれていないバイトを最大256Kまで排出するだけで、全く読まれていないボディのケースだけを最適化しているわけじゃない。 https://cs.opensource.google/go/go/+/refs/tags/go1.27.2:src/... コードを見ると、transfer-encoding:chunkedでのストリーミングレスポンスには対応していないから、もし相手がjson.NewEncoder(responseWriter).Encode(&someStruct)をやっているなら、この最適化は何も効果がないんだ。

Hacker Newsで議論の続きを見る