【ITニュース解説】Go 1.27 Drains Unread Response Bodies for You. Past 256 KB, the Connection Still Dies
2026年11月04日に「Dev.to」が公開したITニュース「Go 1.27 Drains Unread Response Bodies for You. Past 256 KB, the Connection Still Dies」について初心者にもわかりやすく解説しています。
ITニュース概要
Go 1.27では、HTTP/1通信で使い終わった接続を自動で再利用しやすくする機能が追加された。レスポンスボディの未読データを自動で読み捨て、パフォーマンスが向上する。ただし、256KB超や50ms以上かかる場合は対象外で、依然として接続が切れる場合がある。
ITニュース解説
Go言語の最新バージョンであるGo 1.27では、ネットワーク通信、特にHTTPクライアントの挙動に重要な改善が加えられた。この変更は、ウェブサービス開発において非常に頻繁に行われるHTTPリクエストのパフォーマンスと効率に直接影響を与えるものだ。システムエンジニアを目指す皆さんにとって、Go言語でのウェブアプリケーション開発の基本を理解する上で、このアップデートの背景と内容を知ることは非常に有益だろう。
これまでGo言語を使ってHTTPクライアントで外部のAPIやウェブサイトと通信する際、開発者は一つの習慣を身につけていた。それは、HTTPレスポンスのボディを読み終わった後、あるいは途中で読み込みを中止した場合でも、必ずresp.Body.Close()を呼び出す前に、読み残したボディの内容をすべて破棄する処理を手動で追加することだった。具体的には、io.Copy(io.Discard, resp.Body)のようなコードを記述し、読み残しを明示的に捨てる必要があったのだ。もしこの処理を怠ると、Go 1.26以前のバージョンでは、現在のTCP接続が適切にクローズされず、再利用できなくなってしまうという問題があった。
HTTP/1.1プロトコルでは、一つのTCP接続を使い回して複数のリクエストとレスポンスを順番にやり取りすることで、通信のオーバーヘッドを減らし、効率を高める「接続の再利用(Keep-Alive)」が行われる。しかし、クライアントがレスポンスボディの最後まで読み込まなかった場合、サーバーはその接続で次のリクエストを受け付ける準備ができない。Go 1.26以前では、このような状況が発生すると、GoのHTTPクライアントは「この接続はもう使えない」と判断し、その接続を破棄して新しいTCP接続を確立し直していた。新しい接続を確立するには、ネットワーク上で「TCPハンドシェイク」と呼ばれる初期設定のやり取りが必要で、もしHTTPS(暗号化されたHTTP)を使っている場合は、さらに「TLSハンドシェイク」という暗号化のための設定も必要になる。これらの追加の処理は、特にたくさんのリクエストを連続して送るようなケースでは、無視できないほどのパフォーマンス低下を引き起こしていたのだ。例えば、記事のベンチマークでは、Go 1.26で100回の連続GETリクエストに約710ミリ秒かかっていた処理が、Go 1.27では約150ミリ秒にまで短縮されたことが示されており、この改善がいかに大きいかがわかる。
Go 1.27の最大の変更点は、この「読み残したレスポンスボディを破棄する」という処理が、Goの標準ライブラリであるnet/http.Transportに自動的に組み込まれたことだ。つまり、開発者が手動でio.Copy(io.Discard, resp.Body)を書かなくても、resp.Body.Close()を呼び出すだけで、Goのランタイムが自動的に読み残しを処理してくれるようになったのである。これにより、接続の再利用が促進され、特に短いレスポンスを多数処理するようなアプリケーションでは、顕著なパフォーマンス向上とコードの簡潔化が期待できる。
しかし、この自動ドレイン(排出)機能にはいくつかの「制限」が設けられていることに注意が必要だ。すべてのケースで自動的に接続が再利用されるわけではない。主な制限は以下の通りである。
第一に、「レスポンスボディの全体サイズ」に関する制限だ。自動ドレインは、HTTPレスポンスのヘッダーに含まれるContent-Length(ボディの総バイト数を示す情報)が256KB(262,144バイト)以下の場合にのみ試みられる。これは、たとえクライアントがボディのほとんどを読み込み、残りがごく少量(例えば100バイト)であったとしても、元のContent-Lengthが256KBを超えていれば、Go 1.27は自動ドレインを試みず、その接続を破棄してしまうということを意味する。
第二に、「ドレイン処理の時間」に関する制限がある。自動ドレインは、最大50ミリ秒という時間制限の中で行われる。もしサーバーからのレスポンスの残りの部分がこの時間内にすべて読み込めなかった場合、ドレインは中断され、接続は再利用されずに破棄される。これは、通信速度が遅いサーバーや、レスポンスの送信中に一時停止するようなサーバーとのやり取りで影響が出る可能性がある。
第三に、「チャンク転送(Transfer-Encoding: chunked)」の場合の挙動が異なる。Content-Lengthヘッダーがない、つまりボディの総サイズが事前に不明な「チャンク形式」のレスポンスの場合、Go 1.27はドレインを試みる。この場合は、最大で256KBプラス1バイトまでボディを読み込み、EOF(End Of File、データの終わりを示すマーカー)に到達すれば接続を再利用する。しかし、この制限量を超えてもEOFに到達しなかった場合は、やはり接続は破棄される。
第四に、クライアントがリクエスト時にConnection: closeヘッダを明示的に送っている場合や、レスポンスに同様のヘッダが含まれている場合は、接続の再利用が意図的に行われないため、自動ドレインも実行されない。
この自動ドレイン処理は、resp.Body.Close()が呼び出された後、非同期に、つまり裏側で並行して実行される。そのため、Close()の呼び出し自体はすぐに完了し、その後の処理をブロックすることはない。
この変更はHTTP/1.1プロトコルにのみ適用され、HTTP/2には影響しない。HTTP/2は、複数のストリームを一つのTCP接続上で多重化して扱うため、一つのストリームでボディの読み残しがあっても、他のストリームの通信をブロックしないという特性があるためだ。
これまでのコードへの影響について考えてみよう。もしあなたのGoプロジェクトがGo 1.27以降のバージョンのみをサポートしているのであれば、これまで手動で書いていたio.Copy(io.Discard, resp.Body)の行は削除し、単にdefer resp.Body.Close()だけを残すことで、コードをよりシンプルにできる。標準ライブラリのこの自動ドレインは、無制限にボディを読み込むのではなく、上限が設定されているため、予期せぬ大きなボディを誤ってすべて読み込んでしまうといったリスクも減らせる。
しかし、もしGo 1.26以前のバージョンも引き続きサポートする必要がある場合は、手動でのドレイン処理を続ける必要がある。その際も、単にio.Copy(io.Discard, resp.Body)とするのではなく、標準ライブラリの挙動に合わせて、io.CopyN(io.Discard, body, 256<<10+1)のように、読み込むバイト数に上限を設けることが推奨される。これは、無制限の読み込みが、意図せず巨大なファイルをダウンロードしてしまうなどの問題を引き起こす可能性があるためだ。また、ドレイン処理が遅いサーバーによってブロックされないよう、タイムアウト機構を組み合わせることも検討すると良いだろう。
大きなサイズのレスポンスボディを扱う場合や、特定のレスポンスを意図的に途中で破棄し、その接続を再利用しないと決めている場合は、この自動ドレイン機能に頼るべきではない。そのような状況では、req.Close = trueをリクエストに設定することで、HTTPクライアントにConnection: closeヘッダを送信させ、サーバーにも接続を閉じるように伝えるのが適切だ。これにより、不要なドレイン処理を避け、新しい接続を確立するコストの方が、ドレインを試みるよりも安価になる場合がある。
最後に、一部の特殊な設定をしているアプリケーションでは、この変更がパフォーマンスを悪化させる可能性もある。例えば、net/http.TransportのMaxIdleConnsを0に設定したり、リクエストごとに新しいhttp.Clientを作成したりして、意図的に接続の再利用を避けていたアプリケーションだ。Go 1.27で接続が以前よりも生き残るようになったため、これまではすぐに破棄されていた接続がアイドル状態のプールに残り、「過剰なアイドル接続」となってしまうことがある。このような場合は、Transport.DisableKeepAlives = trueを設定して、接続の再利用を完全に無効にすることで対処できる。しかし、これは通常、HTTPクライアントの構成や使い方に根本的な問題があることを示唆している場合が多く、その原因を深く探ることがより良い解決策につながるだろう。多くの場合、解決策はリクエストごとにクライアントを新しく作るのではなく、一つのhttp.Clientインスタンスをアプリケーション全体で共有することだ。
Go 1.27でのこの改善は、Go言語のHTTPクライアントがより賢く、そしてより効率的に動作するようになったことを示している。標準ライブラリのこのような内部的な挙動を理解することは、システムエンジニアとして高性能なアプリケーションを開発する上で非常に重要な知識となる。