第18章: HTTPプログラミング
はじめに
Goのnet/httpパッケージは、HTTPサーバーとクライアントを構築するための強力な機能を提供します。標準ライブラリだけで本格的なWebアプリケーションを作成でき、外部フレームワークは必須ではありません。この章では、HTTPサーバーの実装からRESTful API、ミドルウェアパターンまで、マシンレベルの動作を深く理解しながら学びます。
net/httpの内部アーキテクチャ
HTTPサーバーの起動プロセス
🔑 重要: http.ListenAndServeは内部で複雑な初期化プロセスを経て、TCP接続を受け付けるリスナーを作成します。
package main
import (
"fmt"
"net/http"
)
func handler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
http.HandleFunc("/", handler)
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}
内部で何が起こっているか:
┌─────────────────────────────────────────┐
│ http.ListenAndServe(":8080", nil) │
└───────────────┬─────────────────────────┘
│
v
┌─────────────────────────────────────────┐
│ net.Listen("tcp", ":8080") │ TCP リスナー作成
│ - カーネルにソケット作成を要求 │
│ - ポート8080をバインド │
│ - バックログキュー設定(SOMAXCONN) │
└───────────────┬─────────────────────────┘
│
v
┌─────────────────────────────────────────┐
│ server.Serve(listener) │ メインループ開始
└───────────────┬─────────────────────────┘
│
v
┌─────────────────────────────────────────┐
│ 無限ループ: │
│ for { │
│ conn, _ := listener.Accept() │ ← ブロック
│ go c.serve(connCtx) │ ← 新goroutine
│ } │
└─────────────────────────────────────────┘
💡 理解のポイント: 各接続ごとに新しいgoroutineが生成されるため、同時に何千もの接続を処理できます。これがGoのHTTPサーバーが高性能な理由です。
Server構造体の詳細
type Server struct {
Addr string // TCPアドレス
Handler Handler // ルートハンドラー
TLSConfig *tls.Config // TLS設定
ReadTimeout time.Duration // リクエスト読み取りタイムアウト
WriteTimeout time.Duration // レスポンス書き込みタイムアウト
IdleTimeout time.Duration // Keep-Alive接続のタイムアウト
MaxHeaderBytes int // リクエストヘッダーの最大サイズ
// 内部フィールド
mu sync.Mutex
listeners map[*net.Listener]struct{}
activeConn map[*conn]struct{}
doneChan chan struct{}
}
🔑 activeConnマップ: サーバーは全てのアクティブな接続を追跡しています。これにより、グレースフルシャットダウン時に全接続の終了を待つことができます。
接続処理の内部フロー
クライアント接続 → TCP Handshake → conn.serve() goroutine起動
│
v
┌────────────────────────────────┐
│ 1. TLS Handshake (HTTPSの場合) │
└────────────┬───────────────────┘
│
v
┌────────────────────────────────┐
│ 2. HTTP/1.1 or HTTP/2 判定 │
└────────────┬───────────────────┘
│
v
┌────────────────────────────────┐
│ 3. リクエストパーサー起動 │
│ - Request Line読み取り │
│ - Headers読み取り │
│ - Body読み取り (必要なら) │
└────────────┬───────────────────┘
│
v
┌────────────────────────────────┐
│ 4. ServeMux.ServeHTTP() 呼び出し│
│ - パス照合 │
│ - ハンドラー実行 │
└────────────┬───────────────────┘
│
v
┌────────────────────────────────┐
│ 5. レスポンス書き込み │
│ - Status Line │
│ - Headers │
│ - Body │
└────────────┬───────────────────┘
│
v
┌────────────────────────────────┐
│ 6. 接続管理 │
│ - Keep-Alive判定 │
│ - 接続クローズ or 再利用 │
└────────────────────────────────┘
ResponseWriterの内部実装
ResponseWriterインターフェース
type ResponseWriter interface {
Header() Header // ヘッダーマップを返す
Write([]byte) (int, error) // レスポンスボディを書き込む
WriteHeader(statusCode int) // ステータスコードを書き込む
}
🔑 重要: Write()が最初に呼ばれた時、まだWriteHeader()が呼ばれていなければ、自動的に200 OKが送信されます。
内部バッファリングメカニズム
type response struct {
conn *conn
req *Request
// ヘッダー状態
header Header
wroteHeader bool // WriteHeaderが呼ばれたか
// バッファリング
w *bufio.Writer // 出力バッファ
cw chunkWriter // チャンク書き込み用
// 状態管理
status int // HTTPステータスコード
contentLength int64 // Content-Length
written int64 // 書き込んだバイト数
// フラグ
handlerDone atomicBool
calledHeader bool
}
書き込みプロセスの詳細:
┌────────────────────────────────────────┐
│ w.Write(data) │
└─────────────┬──────────────────────────┘
│
v
┌────────────────────────────────────────┐
│ wroteHeader == false ? │
│ → WriteHeader(200) │
│ - "HTTP/1.1 200 OK\r\n" │
│ - ヘッダー全て書き込み │
│ - "\r\n" (区切り) │
└─────────────┬──────────────────────────┘
│
v
┌────────────────────────────────────────┐
│ Transfer-Encoding: chunked ? │
│ YES → チャンク形式で書き込み │
│ NO → そのまま書き込み │
└─────────────┬──────────────────────────┘
│
v
┌────────────────────────────────────────┐
│ bufio.Writer にバッファリング │
│ - デフォルト4KBバッファ │
│ - Flush条件: │
│ 1. バッファ満杯 │
│ 2. レスポンス完了 │
│ 3. 手動Flush呼び出し │
└────────────────────────────────────────┘
💡 パフォーマンスヒント: 小さなデータを何度も書き込むより、大きなバッファにまとめてから書き込む方が効率的です。
Hijacking - TCPコネクションの乗っ取り
WebSocketなどの実装で使用される高度な機能:
func websocketHandler(w http.ResponseWriter, r *http.Request) {
// ResponseWriterをHijacker型にキャスト
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "Hijacking not supported", http.StatusInternalServerError)
return
}
// 生のTCP接続を取得
conn, bufrw, err := hijacker.Hijack()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// この時点でnet/httpの制御から外れる
// 自分でプロトコルを実装
defer conn.Close()
// WebSocketハンドシェイク
bufrw.WriteString("HTTP/1.1 101 Switching Protocols\r\n")
bufrw.WriteString("Upgrade: websocket\r\n")
bufrw.WriteString("Connection: Upgrade\r\n")
bufrw.WriteString("\r\n")
bufrw.Flush()
// これ以降、WebSocketフレームを直接処理
}
⚠️ 注意: Hijackした後は、HTTPサーバーの管理から完全に外れます。接続のクローズなど全て自分で管理する必要があります。
ServeMuxのルーティングアルゴリズム
内部データ構造
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry // 完全一致用マップ
es []muxEntry // パターンエントリ(スライス)
hosts bool // ホストベースルーティング使用中
}
type muxEntry struct {
h Handler
pattern string
}
パス照合アルゴリズム
リクエストパス: /api/users/123
照合プロセス:
┌─────────────────────────────────────────┐
│ 1. 完全一致を試行 │
│ m["/api/users/123"] を検索 │
│ → 見つからない │
└─────────────┬───────────────────────────┘
│
v
┌─────────────────────────────────────────┐
│ 2. パターンマッチング(長い順) │
│ 登録パターン: │
│ /api/users/ ← 12文字 │
│ /api/ ← 5文字 │
│ / ← 1文字 │
└─────────────┬───────────────────────────┘
│
v
┌─────────────────────────────────────────┐
│ 3. 最長マッチを選択 │
│ /api/users/123 は /api/users/ にマッチ│
│ → このハンドラーを実行 │
└─────────────────────────────────────────┘
🔑 重要: ServeMuxは最長プレフィックスマッチを使用します。より具体的なパターンが優先されます。
トレーリングスラッシュの扱い
mux := http.NewServeMux()
// パターン1: /users/
mux.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
// /users/, /users/123, /users/123/profile 全てマッチ
})
// パターン2: /users
mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
// /users のみ完全一致
})
照合ルール:
リクエスト パターン /users/ パターン /users
/users → リダイレクト → マッチ
/users/ → マッチ → ミスマッチ
/users/123 → マッチ → ミスマッチ
💡 ベストプラクティス: RESTful APIでは明示的にトレーリングスラッシュの有無を統一しましょう。
HTTPリクエストの解析
Requestオブジェクトの構造
type Request struct {
Method string // GET, POST, etc.
URL *url.URL // リクエストURL
Proto string // "HTTP/1.1"
Header Header // HTTPヘッダー
Body io.ReadCloser // リクエストボディ
// 計算済みフィールド
ContentLength int64
TransferEncoding []string
Host string
Form url.Values // 解析済みフォーム
PostForm url.Values // POSTボディのみ
MultipartForm *multipart.Form
// コンテキスト
ctx context.Context
// 接続情報
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
}
リクエストボディの読み取り戦略
🔑 重要: Request.Bodyは一度しか読めません。読んだ後はEOFに達します。
func handleRequest(w http.ResponseWriter, r *http.Request) {
// 方法1: 全て読み込む(小さなペイロード向け)
body, err := io.ReadAll(r.Body)
defer r.Body.Close()
// 方法2: ストリーミング読み取り(大きなペイロード向け)
scanner := bufio.NewScanner(r.Body)
for scanner.Scan() {
line := scanner.Text()
// 行ごとに処理
}
// 方法3: JSONデコーダー(構造化データ向け)
var data MyStruct
err := json.NewDecoder(r.Body).Decode(&data)
}
メモリ使用パターン:
io.ReadAll:
┌────────────────────────────────────┐
│ 全データをメモリに展開 │
│ メモリ使用: ペイロードサイズと同じ │
│ 適用: < 10MB │
└────────────────────────────────────┘
ストリーミング:
┌────────────────────────────────────┐
│ バッファサイズ分だけメモリ使用 │
│ メモリ使用: 固定(例: 4KB) │
│ 適用: 大きなファイルアップロード │
└────────────────────────────────────┘
クエリパラメータとフォームの解析
func searchHandler(w http.ResponseWriter, r *http.Request) {
// URLクエリパラメータ
// GET /search?q=golang&page=2&limit=10
query := r.URL.Query() // url.Valuesを返す
// url.Valuesの内部構造:
// type Values map[string][]string
// 単一の値を取得
keyword := query.Get("q") // "golang"
// 全ての値を取得(配列として)
tags := query["tag"] // []string{"web", "api"}
// 存在チェック
if page, ok := query["page"]; ok {
// page パラメータが存在
}
}
ParseFormの内部動作:
func formHandler(w http.ResponseWriter, r *http.Request) {
// ParseForm呼び出し前:
// r.Form == nil
// r.PostForm == nil
err := r.ParseForm()
// ParseForm呼び出し後:
// r.Form: URLクエリ + POSTボディ(統合)
// r.PostForm: POSTボディのみ
// Content-Type: application/x-www-form-urlencoded の場合
name := r.FormValue("name") // FormとPostFormから検索
email := r.PostFormValue("email") // PostFormのみから検索
}
メモリ効率の比較:
r.FormValue("key"):
→ 内部でParseFormを自動呼び出し
→ 全フォームデータをメモリにパース
query.Get("key"):
→ URLクエリのみパース
→ より軽量
JSONエンコーディングの最適化
Encoderの内部バッファリング
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
func usersHandler(w http.ResponseWriter, r *http.Request) {
users := []User{
{ID: 1, Name: "Alice", Email: "alice@example.com"},
{ID: 2, Name: "Bob", Email: "bob@example.com"},
}
w.Header().Set("Content-Type", "application/json")
// 方法1: Encoder(推奨)
json.NewEncoder(w).Encode(users)
// 方法2: Marshal(非推奨 - 2回のアロケーション)
data, _ := json.Marshal(users)
w.Write(data)
}
パフォーマンス比較:
Encode直接:
┌──────────────────────────────────────┐
│ struct → JSONバイト → ResponseWriter │
│ アロケーション: 1回 │
│ バッファコピー: 0回 │
└──────────────────────────────────────┘
Marshal + Write:
┌──────────────────────────────────────┐
│ struct → 中間バッファ → ResponseWriter│
│ アロケーション: 2回 │
│ バッファコピー: 1回 │
└──────────────────────────────────────┘
💡 最適化のポイント: 大量のJSONレスポンスを返す場合、json.Encoderを直接使うことで、メモリアロケーションとコピーを削減できます。
カスタムMarshalJSON
type Timestamp time.Time
// カスタムJSON出力
func (t Timestamp) MarshalJSON() ([]byte, error) {
stamp := fmt.Sprintf("\"%s\"", time.Time(t).Format("2006-01-02 15:04:05"))
return []byte(stamp), nil
}
type Event struct {
ID int `json:"id"`
Name string `json:"name"`
CreatedAt Timestamp `json:"created_at"`
}
シリアライズプロセス:
Event構造体 → json.Marshal()
│
v
┌───────────────────────────────┐
│ 各フィールドを順番に処理 │
├───────────────────────────────┤
│ ID (int) │
│ → strconv.Itoa() │
├───────────────────────────────┤
│ Name (string) │
│ → エスケープ処理 │
├───────────────────────────────┤
│ CreatedAt (Timestamp) │
│ → MarshalJSON() 呼び出し ← カスタム
└───────────────────────────────┘
│
v
JSON文字列完成
HTTP/2の内部実装
HTTP/1.1 vs HTTP/2
HTTP/1.1:
┌────────────────────────────────────┐
│ 1接続 = 1リクエスト/レスポンス │
│ │
│ リクエスト1 ──────────► │
│ ◄────────── レスポンス1│
│ リクエスト2 ──────────► │
│ ◄────────── レスポンス2│
│ │
│ 問題: Head-of-Line Blocking │
└────────────────────────────────────┘
HTTP/2:
┌────────────────────────────────────┐
│ 1接続 = 多重ストリーム │
│ │
│ Stream 1: リクエスト1 ──────────► │
│ Stream 3: リクエスト2 ──────────► │
│ Stream 1: ◄────────── レスポンス1 │
│ Stream 5: リクエスト3 ──────────► │
│ Stream 3: ◄────────── レスポンス2 │
│ │
│ 利点: 並列処理、優先度制御 │
└────────────────────────────────────┘
HTTP/2フレーム構造
全てのHTTP/2通信はバイナリフレームで行われる:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-+-----------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
主なフレームタイプ:
- DATA (0x0): 実際のデータ
- HEADERS (0x1): HTTPヘッダー
- PRIORITY (0x2): ストリーム優先度
- SETTINGS (0x4): 接続設定
- PING (0x6): 接続確認
- GOAWAY (0x7): 接続終了通知
Server Pushの実装
func handleWithPush(w http.ResponseWriter, r *http.Request) {
// HTTP/2 Pusherにキャスト
pusher, ok := w.(http.Pusher)
if !ok {
// HTTP/1.1 フォールバック
serveWithoutPush(w, r)
return
}
// /style.cssを事前プッシュ
if err := pusher.Push("/style.css", nil); err != nil {
log.Printf("Failed to push: %v", err)
}
// /script.jsを事前プッシュ
if err := pusher.Push("/script.js", nil); err != nil {
log.Printf("Failed to push: %v", err)
}
// メインレスポンス
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, "<html><head><link rel='stylesheet' href='/style.css'>")
fmt.Fprintf(w, "<script src='/script.js'></script></head><body>")
fmt.Fprintf(w, "<h1>Hello with Server Push!</h1></body></html>")
}
Server Pushの流れ:
クライアント サーバー
│ │
│ GET / HTTP/2 │
├───────────────────────────────────────►│
│ │
│ PUSH_PROMISE (stream 2, /style.css)
│◄───────────────────────────────────────┤
│ PUSH_PROMISE (stream 4, /script.js)
│◄───────────────────────────────────────┤
│ │
│ HEADERS + DATA (stream 1, /) │
│◄───────────────────────────────────────┤
│ HEADERS + DATA (stream 2, style.css) │
│◄───────────────────────────────────────┤
│ HEADERS + DATA (stream 4, script.js) │
│◄───────────────────────────────────────┤
💡 最適化ポイント: クリティカルリソース(CSS、重要なJS)をServer Pushすることで、初回ページロードを高速化できます。
コネクションプーリングとKeep-Alive
HTTP/1.1 Keep-Alive
server := &http.Server{
Addr: ":8080",
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second, // Keep-Alive接続の保持時間
}
Keep-Alive接続の状態遷移:
新規接続
│
v
┌──────────────┐
│ ACTIVE │ ← リクエスト処理中
└───┬──────────┘
│ レスポンス完了
v
┌──────────────┐
│ IDLE │ ← 次のリクエスト待ち(IdleTimeout)
└───┬──────────┘
│
├─► 新リクエスト到着 → ACTIVE
│
└─► IdleTimeout経過 → CLOSED
クライアント側のコネクションプール
// デフォルトTransport
http.DefaultTransport = &http.Transport{
MaxIdleConns: 100, // 全ホスト合計の最大アイドル接続数
MaxIdleConnsPerHost: 10, // ホストごとの最大アイドル接続数
MaxConnsPerHost: 100, // ホストごとの最大接続数(アクティブ+アイドル)
IdleConnTimeout: 90 * time.Second, // アイドル接続のタイムアウト
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
}
コネクションプールの動作:
リクエスト発行
│
v
┌────────────────────────────────┐
│ プールにアイドル接続がある? │
├────────────────────────────────┤
│ YES → 接続を再利用 │
│ NO → 新規接続作成 │
└────────┬───────────────────────┘
│
v
┌────────────────────────────────┐
│ リクエスト送信 & レスポンス受信 │
└────────┬───────────────────────┘
│
v
┌────────────────────────────────┐
│ 接続をプールに返却 │
│ - MaxIdleConnsPerHost未満? │
│ YES → プールに追加 │
│ NO → 接続クローズ │
└────────────────────────────────┘
🔑 重要: 同じホストに大量のリクエストを送る場合、MaxIdleConnsPerHostを増やすことでパフォーマンスが向上します。
Connection: close の影響
// Keep-Aliveを無効化
req, _ := http.NewRequest("GET", "https://example.com", nil)
req.Close = true // "Connection: close" ヘッダーを追加
// または
client := &http.Client{
Transport: &http.Transport{
DisableKeepAlives: true, // 全リクエストでKeep-Aliveを無効化
},
}
パフォーマンスへの影響:
Keep-Alive有効:
リクエスト1: TCP Handshake(50ms) + TLS Handshake(100ms) + HTTP(10ms) = 160ms
リクエスト2: HTTP(10ms) = 10ms ← 接続再利用
リクエスト3: HTTP(10ms) = 10ms
合計: 180ms
Keep-Alive無効:
リクエスト1: TCP Handshake + TLS Handshake + HTTP = 160ms
リクエスト2: TCP Handshake + TLS Handshake + HTTP = 160ms
リクエスト3: TCP Handshake + TLS Handshake + HTTP = 160ms
合計: 480ms
ミドルウェアの実装パターン
関数チェーン方式
type Middleware func(http.HandlerFunc) http.HandlerFunc
func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
// 逆順に適用(最後のミドルウェアが最初に実行される)
for i := len(middlewares) - 1; i >= 0; i-- {
f = middlewares[i](f)
}
return f
}
実行順序の理解:
登録: Chain(handler, mw1, mw2, mw3)
実行順序:
リクエスト
│
v
┌────────┐
│ mw1 │ ← 前処理
└───┬────┘
│ next()
v
┌────────┐
│ mw2 │ ← 前処理
└───┬────┘
│ next()
v
┌────────┐
│ mw3 │ ← 前処理
└───┬────┘
│ next()
v
┌────────┐
│handler │ ← 実ハンドラー
└───┬────┘
│ return
v
┌────────┐
│ mw3 │ ← 後処理
└───┬────┘
v
┌────────┐
│ mw2 │ ← 後処理
└───┬────┘
v
┌────────┐
│ mw1 │ ← 後処理
└───┬────┘
v
レスポンス
リクエストIDミドルウェア
func requestIDMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// リクエストIDを生成
requestID := generateRequestID()
// コンテキストに追加
ctx := context.WithValue(r.Context(), "requestID", requestID)
r = r.WithContext(ctx)
// レスポンスヘッダーに追加
w.Header().Set("X-Request-ID", requestID)
next(w, r)
}
}
func generateRequestID() string {
b := make([]byte, 16)
rand.Read(b)
return fmt.Sprintf("%x", b)
}
レスポンスキャプチャーミドルウェア
// ResponseWriterをラップしてステータスコードをキャプチャ
type responseWriter struct {
http.ResponseWriter
statusCode int
written int64
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func (rw *responseWriter) Write(b []byte) (int, error) {
n, err := rw.ResponseWriter.Write(b)
rw.written += int64(n)
return n, err
}
func loggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// ResponseWriterをラップ
rw := &responseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
}
next(rw, r)
// ログ出力
log.Printf(
"%s %s %d %d %v",
r.Method,
r.RequestURI,
rw.statusCode,
rw.written,
time.Since(start),
)
}
}
HTTPクライアントの高度な使い方
タイムアウト設定の階層
client := &http.Client{
Timeout: 30 * time.Second, // 全体のタイムアウト
Transport: &http.Transport{
DialContext: (&net.Dialer{
Timeout: 5 * time.Second, // 接続タイムアウト
KeepAlive: 30 * time.Second,
}).DialContext,
TLSHandshakeTimeout: 10 * time.Second, // TLSハンドシェイク
ResponseHeaderTimeout: 10 * time.Second, // レスポンスヘッダー待機
ExpectContinueTimeout: 1 * time.Second, // 100-continue待機
},
}
タイムアウトの適用範囲:
全体のフロー:
┌────────────────────────────────────────────────────────┐
│ Client.Timeout (30s) │
│ ┌────────────────────────────────────────────────────┐ │
│ │ Dial TLS Request Response Body │ │
│ │ (5s) (10s) Header Header Read │ │
│ │ (10s) │ │
│ └────────────────────────────────────────────────────┘ │
└────────────────────────────────────────────────────────┘
リトライロジックの実装
func retryableRequest(client *http.Client, req *http.Request, maxRetries int) (*http.Response, error) {
var resp *http.Response
var err error
for i := 0; i < maxRetries; i++ {
// Bodyは複数回読めないので、毎回クローン
reqClone := req.Clone(req.Context())
resp, err = client.Do(reqClone)
if err == nil && resp.StatusCode < 500 {
// 成功 or クライアントエラー(リトライしない)
return resp, nil
}
if resp != nil {
resp.Body.Close()
}
// エクスポネンシャルバックオフ
backoff := time.Duration(1<<uint(i)) * time.Second
time.Sleep(backoff)
log.Printf("Retry %d/%d after %v", i+1, maxRetries, backoff)
}
return nil, fmt.Errorf("max retries exceeded: %w", err)
}
バックオフ戦略:
リトライ回数 待機時間
1 2^0 = 1秒
2 2^1 = 2秒
3 2^2 = 4秒
4 2^3 = 8秒
5 2^4 = 16秒
カスタムRoundTripper
// 全リクエストにカスタムヘッダーを追加
type customTransport struct {
Transport http.RoundTripper
Headers map[string]string
}
func (t *customTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// リクエストをクローン(オリジナルを変更しない)
req = req.Clone(req.Context())
// カスタムヘッダーを追加
for key, value := range t.Headers {
req.Header.Set(key, value)
}
// タイミング計測
start := time.Now()
resp, err := t.Transport.RoundTrip(req)
duration := time.Since(start)
log.Printf("%s %s: %v", req.Method, req.URL, duration)
return resp, err
}
func main() {
client := &http.Client{
Transport: &customTransport{
Transport: http.DefaultTransport,
Headers: map[string]string{
"User-Agent": "MyApp/1.0",
"X-API-Key": "secret",
},
},
}
resp, _ := client.Get("https://api.example.com/data")
defer resp.Body.Close()
}
グレースフルシャットダウン
シャットダウンのシグナル処理
func main() {
server := &http.Server{
Addr: ":8080",
Handler: myHandler,
}
// シグナルチャネル
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
// サーバー起動(別goroutine)
go func() {
log.Println("Starting server on :8080")
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// シグナル待機
<-stop
log.Println("Shutting down server...")
// グレースフルシャットダウン
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server stopped")
}
シャットダウンプロセス:
SIGTERM受信
│
v
┌──────────────────────────────┐
│ 1. 新規接続の受付停止 │
│ listener.Close() │
└────────┬─────────────────────┘
│
v
┌──────────────────────────────┐
│ 2. アクティブ接続の完了待機 │
│ - 進行中リクエスト処理 │
│ - Keep-Alive接続クローズ │
└────────┬─────────────────────┘
│
├─► 30秒以内に完了 → 正常終了
│
└─► タイムアウト → 強制終了
セキュリティのベストプラクティス
HTTPS/TLSの設定
func main() {
// 本番環境向けTLS設定
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}
server := &http.Server{
Addr: ":443",
Handler: myHandler,
TLSConfig: tlsConfig,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
log.Fatal(server.ListenAndServeTLS("cert.pem", "key.pem"))
}
セキュリティヘッダー
func securityHeadersMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// XSS対策
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
// HTTPS強制
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
// Content Security Policy
w.Header().Set("Content-Security-Policy", "default-src 'self'")
next(w, r)
}
}
自己診断問題
以下の問題に答えて、理解度を確認しましょう:
- 基礎:
http.ListenAndServe(":8080", nil)が内部で行う3つの主要な処理を説明してください。 - ResponseWriter:
w.Write()を呼ぶ前にw.WriteHeader()を呼ばなかった場合、何が起こりますか? - ServeMux: パターン
/api/と/api/usersが両方登録されているとき、/api/users/123へのリクエストはどちらにマッチしますか? - Request.Body: なぜ
Request.Bodyは一度しか読めないのですか?内部実装の観点から説明してください。 - Keep-Alive: HTTP/1.1のKeep-Aliveを使うことで、どのようなオーバーヘッドが削減されますか?
- HTTP/2: HTTP/2のストリーム多重化は、HTTP/1.1のHead-of-Line Blocking問題をどう解決しますか?
- コネクションプール:
MaxIdleConnsPerHostを10から100に増やすと、どのような場合にパフォーマンスが向上しますか? - ミドルウェア:
Chain(handler, mw1, mw2, mw3)で登録したミドルウェアの実行順序を図示してください。 - タイムアウト:
Client.TimeoutとTransport.ResponseHeaderTimeoutの違いは何ですか? - グレースフルシャットダウン:
server.Shutdown(ctx)が行う具体的な処理を、内部実装に基づいて説明してください。 - Hijacking:
http.Hijackerを使ってTCP接続を乗っ取った後、HTTPサーバーの責任範囲はどう変わりますか? - JSON最適化:
json.Marshal() + w.Write()よりjson.NewEncoder(w).Encode()の方が効率的な理由を、メモリアロケーションの観点から説明してください。 - 内部アーキテクチャ: 各接続ごとに新しいgoroutineが起動し、並行処理を実現
- ResponseWriter: バッファリングとチャンクエンコーディングの内部実装
- ServeMux: 最長プレフィックスマッチングアルゴリズム
- HTTP/2: バイナリフレームとストリーム多重化
- コネクションプール: Keep-Aliveとアイドル接続管理
- ミドルウェア: 関数チェーンによる横断的関心事の実装
- タイムアウト: 階層的なタイムアウト設定
- グレースフルシャットダウン: 安全なサーバー停止
まとめ
本章では、GoのHTTPプログラミングをマシンレベルで深く学びました。
🔑 重要ポイント:
💡 次のステップ: 次章では、contextパッケージを使った、より高度な処理制御とキャンセル管理を学びます。HTTPハンドラーでのcontext活用は、本番環境で不可欠です。
⚠️ 本番環境への注意: HTTPサーバーを本番環境にデプロイする際は、必ずタイムアウト設定、TLS設定、セキュリティヘッダー、グレースフルシャットダウンを実装してください。