Web開発 - 解説

1. net/httpの内部アーキテクチャ

1.1 HTTPサーバーの動作原理

Goのnet/httpパッケージは、シンプルなAPIの背後に洗練されたアーキテクチャを持っています。

http.ListenAndServe(":8080", handler)

この1行で何が起こっているのか:

  • TCPリスナーの起動
   // 内部的には以下のような処理
   ln, err := net.Listen("tcp", ":8080")
   // ポート8080でTCP接続を待機
   

  • Accept ループ
   for {
       conn, err := ln.Accept()  // 新しい接続を待つ
       go serve(conn, handler)   // 各接続を別goroutineで処理
   }
   

  • リクエストの解析
- HTTPリクエストラインのパース(GET /path HTTP/1.1) - ヘッダーの読み取りと解析 - ボディの読み取り(必要に応じて)

  • ハンドラの呼び出し
   handler.ServeHTTP(w, r)
   

1.2 コネクションの管理

Keep-Alive の処理

srv := &http.Server{
    IdleTimeout: 60 * time.Second,  // アイドル接続の保持時間
}

HTTPのKeep-Aliveにより、同じTCP接続で複数のリクエストを処理できます:

クライアント                   サーバー
    |                              |
    |--- TCP接続確立 ------------->|
    |                              |
    |--- GET /api/users ---------->|
    |<-- 200 OK -------------------|
    |                              |
    |--- GET /api/posts ---------->| (同じTCP接続を再利用)
    |<-- 200 OK -------------------|
    |                              |
    |--- 接続クローズ ------------->|

接続プール データベース接続では、接続プールが重要です:

db.SetMaxOpenConns(25)      // 最大25の同時接続
db.SetMaxIdleConns(5)       // アイドル状態で保持する接続数
db.SetConnMaxLifetime(5*time.Minute)  // 接続の最大寿命

なぜ接続プールが必要か:

  • TCP接続の確立はコストが高い(3-way handshake)
  • データベース認証のオーバーヘッド
  • リソースの効率的な利用

2. Handler, HandlerFunc, ServeMuxの違い

2.1 http.Handler インターフェース

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

任意の型をハンドラにできる:

type MyHandler struct {
    db *sql.DB
}

func (h *MyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // リクエストの処理
    fmt.Fprintf(w, "Hello from MyHandler")
}

// 使用例
handler := &MyHandler{db: db}
http.ListenAndServe(":8080", handler)

2.2 http.HandlerFunc 型

関数をハンドラに変換するアダプター:

type HandlerFunc func(ResponseWriter, *Request)

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)  // 関数を呼び出すだけ
}

使用例:

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, World!")
}

// 関数をHandlerに変換
http.Handle("/", http.HandlerFunc(helloHandler))

// HandleFuncは内部的にHandlerFuncを使う
http.HandleFunc("/", helloHandler)  // より簡潔

2.3 http.ServeMux - ルーター

複数のハンドラを管理するルーター:

mux := http.NewServeMux()
mux.HandleFunc("/users", usersHandler)
mux.HandleFunc("/posts", postsHandler)

パターンマッチングのルール(Go 1.22+):

mux.HandleFunc("GET /users/{id}", getUserHandler)
// ✓ GET /users/123     → マッチ
// ✗ POST /users/123    → マッチしない
// ✗ GET /users/123/abc → マッチしない

mux.HandleFunc("/static/", staticHandler)
// ✓ /static/css/style.css  → マッチ(プレフィックス)
// ✓ /static/js/app.js      → マッチ

優先順位:

  • 完全一致(Exact match)
  • 長いパターン優先
  • 登録順

mux.HandleFunc("/api/users/me", meHandler)        // 最優先
mux.HandleFunc("/api/users/{id}", userHandler)    // 次
mux.HandleFunc("/api/", apiHandler)               // 最後

3. Contextの正しい使い方

3.1 Contextの基本

Contextは以下の情報を運びます:

  • キャンセルシグナル: 処理の中断要求
  • タイムアウト: 処理の期限
  • 値の伝播: リクエストスコープのデータ

func handler(w http.ResponseWriter, r *http.Request) {
    // リクエストには既にContextが含まれている
    ctx := r.Context()

    // タイムアウト付きContextの作成
    ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
    defer cancel()  // 必ず呼ぶ(リソースリーク防止)

    // Contextを使った処理
    result, err := fetchDataWithContext(ctx)
    if err != nil {
        if errors.Is(err, context.DeadlineExceeded) {
            http.Error(w, "Request timeout", http.StatusGatewayTimeout)
            return
        }
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    json.NewEncoder(w).Encode(result)
}

3.2 データベースクエリでのContext

NG例:

rows, err := db.Query("SELECT * FROM users WHERE active = true")
// タイムアウトなし、キャンセル不可

OK例:

ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()

rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE active = true")
// 3秒でタイムアウト、リクエストキャンセル時に自動終了

3.3 HTTPリクエストでのContext

外部APIを呼び出す場合:

func callExternalAPI(ctx context.Context, url string) (*Response, error) {
    req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
    if err != nil {
        return nil, err
    }

    client := &http.Client{
        Timeout: 10 * time.Second,
    }

    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    // レスポンスの処理...
    return parseResponse(resp)
}

Context伝播の例:

HTTPリクエスト
   ↓ (Context作成)
ハンドラ
   ↓ (Context伝播)
ビジネスロジック
   ↓ (Context伝播)
データベースクエリ
   ↓ (Context伝播)
外部APIコール

全ての層でContextが伝播され、ユーザーが接続を切断すると、全ての処理がキャンセルされます。

3.4 Contextに値を保存する

認証情報の伝播:

type contextKey string

const UserIDKey contextKey = "userID"

// ミドルウェアで値を設定
func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        userID := extractUserID(r)  // トークンから抽出
        ctx := context.WithValue(r.Context(), UserIDKey, userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// ハンドラで値を取得
func handler(w http.ResponseWriter, r *http.Request) {
    userID := r.Context().Value(UserIDKey).(string)
    // userIDを使った処理...
}

注意点:

  • Contextの値はリクエストスコープのデータのみ
  • 関数の引数で渡せるものはContextに入れない
  • 型安全性のため、専用の型を定義する
  • 4. パフォーマンス最適化

    4.1 接続プールのチューニング

    db.SetMaxOpenConns(25)
    db.SetMaxIdleConns(5)
    db.SetConnMaxLifetime(5 * time.Minute)
    

    最適な値の決定:

  • MaxOpenConns: サーバーのCPUコア数 × 2〜4
- 少なすぎる: 接続待ちでボトルネック - 多すぎる: データベースサーバーの負荷増

  • MaxIdleConns: MaxOpenConnsの20-30%
- 接続の再利用とリソース消費のバランス

  • ConnMaxLifetime: 5分程度
- 長すぎる: 不健全な接続が残る - 短すぎる: 再接続のオーバーヘッド

4.2 sync.Pool によるメモリ最適化

頻繁に生成・破棄されるオブジェクトのプール:

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func handler(w http.ResponseWriter, r *http.Request) {
    // Poolからバッファを取得
    buf := bufferPool.Get().(*bytes.Buffer)
    defer bufferPool.Put(buf)  // 使用後にPoolに戻す
    buf.Reset()  // 内容をクリア

    // バッファを使った処理
    buf.WriteString("Hello, ")
    buf.WriteString("World!")
    w.Write(buf.Bytes())
}

効果:

  • ガベージコレクションの負荷軽減
  • メモリアロケーションの削減
  • 高スループットアプリケーションで効果大

4.3 JSONエンコーディングの最適化

標準的な方法:

data := map[string]interface{}{
    "name": "John",
    "age":  30,
}
json.NewEncoder(w).Encode(data)  // リフレクション使用

高速化(構造体を使用):

type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

user := User{Name: "John", Age: 30}
json.NewEncoder(w).Encode(user)  // リフレクション最小化

さらに高速化(easyjson等):

go get github.com/mailru/easyjson
easyjson -all user.go  # コード生成

// 生成されたコードを使用
user.MarshalJSON()  // 標準のjson.Marshalより5-10倍高速

4.4 レスポンスの圧縮

import "compress/gzip"

func gzipMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
            next.ServeHTTP(w, r)
            return
        }

        gz := gzip.NewWriter(w)
        defer gz.Close()

        w.Header().Set("Content-Encoding", "gzip")
        gzw := &gzipResponseWriter{Writer: gz, ResponseWriter: w}
        next.ServeHTTP(gzw, r)
    })
}

type gzipResponseWriter struct {
    io.Writer
    http.ResponseWriter
}

func (w *gzipResponseWriter) Write(b []byte) (int, error) {
    return w.Writer.Write(b)
}

効果:

  • 転送データ量: 70-90%削減(テキストベース)
  • ネットワーク帯域の節約
  • レスポンス時間の短縮(特にモバイル)

4.5 キャッシング戦略

インメモリキャッシュ:

import "github.com/patrickmn/go-cache"

var appCache = cache.New(5*time.Minute, 10*time.Minute)

func handler(w http.ResponseWriter, r *http.Request) {
    key := r.URL.Path

    // キャッシュチェック
    if cached, found := appCache.Get(key); found {
        w.Write(cached.([]byte))
        return
    }

    // データ取得
    data := fetchData()

    // キャッシュに保存
    appCache.Set(key, data, cache.DefaultExpiration)
    w.Write(data)
}

HTTPキャッシュヘッダー:

func handler(w http.ResponseWriter, r *http.Request) {
    // ブラウザキャッシュの指示
    w.Header().Set("Cache-Control", "public, max-age=3600")
    w.Header().Set("ETag", generateETag(data))

    // 条件付きリクエストの処理
    if r.Header.Get("If-None-Match") == etag {
        w.WriteHeader(http.StatusNotModified)
        return
    }

    // 通常のレスポンス
    w.Write(data)
}

5. 実装のポイント

5.1 標準ライブラリの活用

Go 1.22から導入された新しいルーティング機能を使用しています:

mux.HandleFunc("GET /api/tasks/{id}", handler)

これにより、外部ライブラリなしで以下が可能です:

  • HTTPメソッドベースのルーティング
  • パスパラメータの抽出
  • RESTful APIの設計

5.2 ミドルウェアパターン

Goのミドルウェアパターンは、http.Handlerをラップする関数です:

func Middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 前処理
        next.ServeHTTP(w, r)
        // 後処理
    })
}

利点:

  • 横断的関心事(ロギング、認証など)の分離
  • 再利用可能なコンポーネント
  • テストしやすい構造

5.3 構造化とパッケージ分割

- handlers/   → HTTPハンドラ
- models/     → ドメインモデル
- storage/    → データアクセス層
- middleware/ → 横断的関心事

設計判断:

  • 責任の明確な分離
  • 依存関係の方向が一方向(handlers → storage → models)
  • テストしやすい構造

5.4 エラーハンドリング

複数のエラータイプを使い分けています:

// カスタムエラー
var ErrTaskNotFound = fmt.Errorf("task not found")

// 構造化されたエラー
type ValidationError struct {
    Field   string
    Message string
}

エラーハンドリングの階層:

  • バリデーションエラー → 400 Bad Request
  • NotFoundエラー → 404 Not Found
  • その他のエラー → 500 Internal Server Error

5.5 並行安全性

MemoryStoresync.RWMutexを使用して並行アクセスに対応:

func (s *MemoryStore) Get(id string) (*models.Task, error) {
    s.mu.RLock()         // 読み取りロック
    defer s.mu.RUnlock()
    // ...
}

func (s *MemoryStore) Create(task *models.Task) error {
    s.mu.Lock()          // 書き込みロック
    defer s.mu.Unlock()
    // ...
}

重要ポイント:

  • 読み取りは複数同時可能(RLock)
  • 書き込みは排他的(Lock)
  • deferで必ずアンロック

6. CS基礎との関連

6.1 ネットワークプログラミング

TCP/IPスタック:

Application Layer (HTTP)
  ↓
Transport Layer (TCP)
  ↓
Internet Layer (IP)
  ↓
Link Layer

HTTPリクエストの流れ:

  • DNS解決: ドメイン名 → IPアドレス
  • TCP接続: 3-way handshake
  • HTTPリクエスト送信: アプリケーション層
  • HTTPレスポンス受信: サーバーからの応答
  • 接続クローズ: または Keep-Alive
  • 6.2 データ構造

    使用されるデータ構造:

  • マップ(ハッシュテーブル)
   tasks map[string]*models.Task
   
- O(1)の検索・挿入・削除 - 平均的な性能は優れているが、最悪ケースはO(n)

  • スライス(動的配列)
   tasks := make([]*models.Task, 0)
   
- 順序付きコレクション - 追加はO(1)(容量が十分な場合)

  • ツリー(B-tree)
- データベースインデックスで使用 - O(log n)の検索・挿入・削除

6.3 並行処理モデル

ゴルーチンの実装:

  • グリーンスレッド(ユーザー空間スレッド)
  • M:N スケジューリング(M個のOSスレッドでN個のゴルーチンを実行)
  • スタックサイズ: 2KB(OSスレッドは1-2MB)

チャネルの実装:

  • ロックとセマフォを使った同期機構
  • バッファ付き/なしチャネル
  • select文による多重化

7. 発展的学習への道筋

7.1 Webフレームワークの選択肢

Gin - 最も人気のフレームワーク

import "github.com/gin-gonic/gin"

r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
    id := c.Param("id")
    c.JSON(200, gin.H{"id": id})
})

特徴:

  • 高速(httprouterベース)
  • 豊富なミドルウェア
  • JSONバリデーション組み込み

Echo - シンプルで拡張性が高い

import "github.com/labstack/echo/v4"

e := echo.New()
e.GET("/users/:id", func(c echo.Context) error {
    return c.JSON(200, map[string]string{"id": c.Param("id")})
})

Fiber - Express.js風のAPI

import "github.com/gofiber/fiber/v2"

app := fiber.New()
app.Get("/users/:id", func(c *fiber.Ctx) error {
    return c.JSON(fiber.Map{"id": c.Params("id")})
})

7.2 マイクロサービスアーキテクチャ

サービス間通信:

  • REST API - 標準的なHTTP通信
  • gRPC - 高速なRPC通信
   service UserService {
       rpc GetUser(UserRequest) returns (UserResponse);
   }
   

  • メッセージキュー - 非同期通信
- RabbitMQ - Apache Kafka - NATS

サービスディスカバリ:

  • Consul
  • etcd
  • Kubernetes Service

API Gateway:

クライアント
    ↓
API Gateway (認証、ルーティング、レート制限)
    ↓
 ┌──┴──┬──────┬──────┐
User   Post  Comment  Payment
Service Service Service Service

7.3 クラウドネイティブ開発

Docker化:

FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o app

FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/app /app
CMD ["/app"]

Kubernetes デプロイ:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: api
        image: myapp:latest
        ports:
        - containerPort: 8080

Observability(可観測性):

  • ロギング: 構造化ログ(JSON)
  • メトリクス: Prometheus
  • トレーシング: Jaeger, OpenTelemetry
  • import "go.opentelemetry.io/otel"
    
    func handler(w http.ResponseWriter, r *http.Request) {
        ctx, span := otel.Tracer("api").Start(r.Context(), "handleRequest")
        defer span.End()
    
        // 処理...
    }
    

    7.4 セキュリティのベストプラクティス

  • 入力バリデーション: SQLインジェクション、XSS対策
  • 認証・認可: JWT、OAuth2.0
  • HTTPS: TLS 1.3
  • レート制限: DDoS対策
  • CORS: クロスオリジン制御
  • セキュリティヘッダー: CSP, HSTS

7.5 推奨学習パス

初級(1-2ヶ月):

  • 標準ライブラリの習得
  • 簡単なCRUD APIの作成
  • ミドルウェアの理解

中級(3-6ヶ月):

  • データベース統合(PostgreSQL)
  • 認証・認可の実装
  • テストの充実
  • Dockerコンテナ化

上級(6ヶ月以上):

  • マイクロサービス設計
  • gRPC実装
  • Kubernetes運用
  • 分散トレーシング
  • パフォーマンスチューニング
  • 次のステップ

    さらに学びを深めるために:

  • 実践プロジェクト: 自分のアイデアでAPIを構築
  • OSSへの貢献: 人気フレームワークのコード読解
  • 本番運用: クラウドへのデプロイと監視
  • コミュニティ: Go Conference、meetupへの参加
  • 継続学習: 新しいパターン、ツールの探索