概要
- 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
}
- 上記のような シンプルなコード で高パフォーマンスを享受可能