第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.TimeoutTransport.ResponseHeaderTimeoutの違いは何ですか?
  • グレースフルシャットダウン: server.Shutdown(ctx)が行う具体的な処理を、内部実装に基づいて説明してください。
  • Hijacking: http.Hijackerを使ってTCP接続を乗っ取った後、HTTPサーバーの責任範囲はどう変わりますか?
  • JSON最適化: json.Marshal() + w.Write()よりjson.NewEncoder(w).Encode()の方が効率的な理由を、メモリアロケーションの観点から説明してください。
  • まとめ

    本章では、GoのHTTPプログラミングをマシンレベルで深く学びました。

    🔑 重要ポイント

  • 内部アーキテクチャ: 各接続ごとに新しいgoroutineが起動し、並行処理を実現
  • ResponseWriter: バッファリングとチャンクエンコーディングの内部実装
  • ServeMux: 最長プレフィックスマッチングアルゴリズム
  • HTTP/2: バイナリフレームとストリーム多重化
  • コネクションプール: Keep-Aliveとアイドル接続管理
  • ミドルウェア: 関数チェーンによる横断的関心事の実装
  • タイムアウト: 階層的なタイムアウト設定
  • グレースフルシャットダウン: 安全なサーバー停止

💡 次のステップ: 次章では、contextパッケージを使った、より高度な処理制御とキャンセル管理を学びます。HTTPハンドラーでのcontext活用は、本番環境で不可欠です。

⚠️ 本番環境への注意: HTTPサーバーを本番環境にデプロイする際は、必ずタイムアウト設定、TLS設定、セキュリティヘッダー、グレースフルシャットダウンを実装してください。