Day 7: 実践プロジェクト - 解説

統合的な実践演習の意義

Day 7は、これまでの6日間で学んだ全ての概念を統合し、実際のプロダクショングレードアプリケーションを構築する総合演習です。

---

アーキテクチャ設計の原則

レイヤー分離アーキテクチャ

┌─────────────────────────────────┐
│     HTTP Handler Layer          │  ← リクエスト/レスポンス処理
├─────────────────────────────────┤
│     Business Logic Layer        │  ← ドメインロジック
├─────────────────────────────────┤
│     Data Access Layer           │  ← データ永続化
└─────────────────────────────────┘

このアーキテクチャにより:

  • 関心の分離: 各レイヤーが明確な責任を持つ
  • テスタビリティ: 各レイヤーを独立してテスト可能
  • 保守性: 変更の影響範囲が限定される
  • 拡張性: 新機能の追加が容易

---

コア概念の統合

1. 型システムとインターフェース(Day 1-3)

// Domain Model - 型安全性を最大限活用
type Todo struct {
    ID          int       `json:"id"`
    Title       string    `json:"title"`
    Description string    `json:"description,omitempty"`
    Completed   bool      `json:"completed"`
    Priority    int       `json:"priority"`
    CreatedAt   time.Time `json:"created_at"`
    UpdatedAt   time.Time `json:"updated_at"`
}

// リクエスト検証 - メソッドによるカプセル化
func (r *CreateTodoRequest) Validate() error {
    if strings.TrimSpace(r.Title) == "" {
        return errors.New("title is required")
    }
    if len(r.Title) > 200 {
        return errors.New("title must be less than 200 characters")
    }
    if r.Priority < 0 || r.Priority > 5 {
        return errors.New("priority must be between 0 and 5")
    }
    return nil
}

学んだこと:

  • 構造体タグによるJSON変換の制御
  • omitemptyによる柔軟なシリアライゼーション
  • メソッドレシーバーによるドメインロジックのカプセル化
  • エラー処理のベストプラクティス
  • 2. 並行処理とスレッドセーフティ(Day 4-5)

    type TodoStore struct {
        mu     sync.RWMutex  // 読み取り/書き込みロック
        todos  map[int]*Todo
        nextID int
    }
    
    // 読み取り専用操作 - RLockを使用
    func (s *TodoStore) Get(id int) (*Todo, error) {
        s.mu.RLock()         // 複数の読み取りを許可
        defer s.mu.RUnlock()  // deferで確実にアンロック
    
        todo, exists := s.todos[id]
        if !exists {
            return nil, errors.New("todo not found")
        }
        return todo, nil
    }
    
    // 書き込み操作 - Lockを使用
    func (s *TodoStore) Create(req CreateTodoRequest) (*Todo, error) {
        if err := req.Validate(); err != nil {
            return nil, err
        }
    
        s.mu.Lock()          // 排他的ロック
        defer s.mu.Unlock()   // deferで確実にアンロック
    
        s.nextID++
        todo := &Todo{
            ID:          s.nextID,
            Title:       strings.TrimSpace(req.Title),
            Description: strings.TrimSpace(req.Description),
            Completed:   false,
            Priority:    req.Priority,
            CreatedAt:   time.Now(),
            UpdatedAt:   time.Now(),
        }
    
        s.todos[todo.ID] = todo
        return todo, nil
    }
    

    重要なポイント:

  • RWMutex vs Mutex:
- RLock: 複数の読み取りゴルーチンが同時アクセス可能 - Lock: 1つの書き込みゴルーチンのみがアクセス可能 - パフォーマンス最適化に有効

  • deferによるロック解放:
- パニックが発生してもロックが解放される - 早期リターンでもロック解放を忘れない - Goのイディオマティックなパターン

  • データ競合の防止:
- 共有状態への全アクセスをロックで保護 - ロックの粒度を適切に設定 - デッドロックを避ける設計

3. HTTPハンドラーとルーティング

// http.Handlerインターフェースの実装
func (h *TodoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // CORS設定
    w.Header().Set("Access-Control-Allow-Origin", "*")
    w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
    w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

    // プリフライトリクエスト処理
    if r.Method == http.MethodOptions {
        w.WriteHeader(http.StatusOK)
        return
    }

    // パスベースルーティング
    path := strings.TrimPrefix(r.URL.Path, "/api/todos")

    switch {
    case path == "" || path == "/":
        h.handleTodos(w, r)  // コレクション操作
    case strings.HasPrefix(path, "/"):
        h.handleTodoByID(w, r, strings.TrimPrefix(path, "/"))  // リソース操作
    default:
        http.NotFound(w, r)
    }
}

RESTful API設計の原則:

  • リソース指向:
- /api/todos - コレクション - /api/todos/{id} - 単一リソース

  • HTTPメソッドの意味:
- GET: 読み取り(冪等) - POST: 作成(非冪等) - PUT: 更新(冪等) - DELETE: 削除(冪等)

  • 適切なステータスコード:
- 200 OK: 成功(GET, PUT) - 201 Created: 作成成功(POST) - 204 No Content: 成功、レスポンスボディなし(DELETE) - 400 Bad Request: クライアントエラー - 404 Not Found: リソース未存在 - 500 Internal Server Error: サーバーエラー

4. ミドルウェアパターン

// ミドルウェアの型定義
type Middleware func(http.Handler) http.Handler

// ロギングミドルウェア
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // カスタムResponseWriterでステータスコードをキャプチャ
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}

        next.ServeHTTP(wrapped, r)

        log.Printf(
            "%s %s %d %s",
            r.Method,
            r.URL.Path,
            wrapped.statusCode,
            time.Since(start),
        )
    })
}

// リカバリーミドルウェア
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                log.Printf("panic: %v", err)
                respondError(w, http.StatusInternalServerError, "internal server error")
            }
        }()
        next.ServeHTTP(w, r)
    })
}

// ミドルウェアのチェーン
handler := recoveryMiddleware(loggingMiddleware(mux))

ミドルウェアの利点:

  • 横断的関心事の分離: ロギング、認証、リカバリーなど
  • 再利用性: 複数のハンドラーで共通のロジックを共有
  • 合成可能: 複数のミドルウェアを組み合わせ可能
  • 順序制御: ミドルウェアの適用順序を明示的に制御
  • 5. Graceful Shutdown

    // サーバー設定
    server := &http.Server{
        Addr:         ":8080",
        Handler:      handler,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }
    
    // 非同期でサーバー起動
    go func() {
        log.Printf("Server starting on %s", server.Addr)
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Server failed: %v", err)
        }
    }()
    
    // シグナル待機
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt)
    <-quit
    
    log.Println("Shutting down server...")
    
    // グレースフルシャットダウン
    ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()
    
    if err := server.Shutdown(ctx); err != nil {
        log.Fatalf("Server forced to shutdown: %v", err)
    }
    
    log.Println("Server exited")
    

    グレースフルシャットダウンの重要性:

  • 処理中のリクエストを完了: 既存の接続を強制終了しない
  • 新しいリクエストを拒否: 新規接続を受け付けない
  • タイムアウト制御: 一定時間後に強制終了
  • リソースクリーンアップ: データベース接続などを適切に閉じる

---

テスト戦略

1. ユニットテスト

func TestTodoStore_Create(t *testing.T) {
    store := NewTodoStore()

    tests := []struct {
        name    string
        req     CreateTodoRequest
        wantErr bool
    }{
        {
            name: "valid todo",
            req: CreateTodoRequest{
                Title:    "Test Todo",
                Priority: 1,
            },
            wantErr: false,
        },
        {
            name: "empty title",
            req: CreateTodoRequest{
                Title:    "",
                Priority: 1,
            },
            wantErr: true,
        },
        {
            name: "invalid priority",
            req: CreateTodoRequest{
                Title:    "Test",
                Priority: 10,
            },
            wantErr: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            todo, err := store.Create(tt.req)
            if (err != nil) != tt.wantErr {
                t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
                return
            }
            if !tt.wantErr && todo.Title != tt.req.Title {
                t.Errorf("Title = %v, want %v", todo.Title, tt.req.Title)
            }
        })
    }
}

テーブル駆動テスト:

  • 複数のテストケースを構造的に管理
  • コードの重複を削減
  • テストケースの追加が容易

2. HTTPハンドラーテスト

func TestTodoHandler_CreateTodo(t *testing.T) {
    store := NewTodoStore()
    handler := NewTodoHandler(store)

    reqBody := CreateTodoRequest{
        Title:       "Test Todo",
        Description: "Test Description",
        Priority:    2,
    }

    body, _ := json.Marshal(reqBody)
    req := httptest.NewRequest(http.MethodPost, "/api/todos", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")

    rr := httptest.NewRecorder()
    handler.ServeHTTP(rr, req)

    // ステータスコード検証
    if status := rr.Code; status != http.StatusCreated {
        t.Errorf("handler returned wrong status code: got %v want %v",
            status, http.StatusCreated)
    }

    // レスポンスボディ検証
    var todo Todo
    if err := json.NewDecoder(rr.Body).Decode(&todo); err != nil {
        t.Errorf("failed to decode response: %v", err)
    }

    if todo.Title != reqBody.Title {
        t.Errorf("Title = %v, want %v", todo.Title, reqBody.Title)
    }
}

httptest パッケージ:

  • httptest.NewRequest: テスト用HTTPリクエスト作成
  • httptest.NewRecorder: レスポンスをキャプチャ
  • 実際のHTTPサーバーを起動せずにテスト可能

3. 並行性テスト

func TestConcurrentAccess(t *testing.T) {
    store := NewTodoStore()

    const goroutines = 100
    done := make(chan bool)

    // 100個のゴルーチンで同時書き込み
    for i := 0; i < goroutines; i++ {
        go func(id int) {
            store.Create(CreateTodoRequest{
                Title:    fmt.Sprintf("Todo %d", id),
                Priority: 1,
            })
            done <- true
        }(i)
    }

    // 全ゴルーチンの完了を待機
    for i := 0; i < goroutines; i++ {
        <-done
    }

    todos := store.List(nil)
    if len(todos) != goroutines {
        t.Errorf("expected %d todos, got %d", goroutines, len(todos))
    }
}

並行性テストのポイント:

  • データ競合の検出: go test -race
  • 複数ゴルーチンの同期
  • エッジケースの発見

4. ベンチマーク

func BenchmarkTodoStore_Create(b *testing.B) {
    store := NewTodoStore()
    req := CreateTodoRequest{
        Title:    "Benchmark Todo",
        Priority: 1,
    }

    b.ResetTimer()  // セットアップ時間を除外
    for i := 0; i < b.N; i++ {
        store.Create(req)
    }
}

func BenchmarkConcurrentCreate(b *testing.B) {
    store := NewTodoStore()
    req := CreateTodoRequest{
        Title:    "Concurrent Todo",
        Priority: 1,
    }

    b.RunParallel(func(pb *testing.PB) {
        for pb.Next() {
            store.Create(req)
        }
    })
}

ベンチマーク実行:

go test -bench=. -benchmem

出力例:

BenchmarkTodoStore_Create-8          1000000    1234 ns/op    256 B/op    4 allocs/op
BenchmarkConcurrentCreate-8          5000000     456 ns/op    128 B/op    2 allocs/op

---

パフォーマンス最適化

1. メモリ割り当ての最小化

// 悪い例: 頻繁なメモリ割り当て
func List() []Todo {
    result := []Todo{}
    for _, todo := range s.todos {
        result = append(result, *todo)
    }
    return result
}

// 良い例: 容量を事前確保
func List() []*Todo {
    result := make([]*Todo, 0, len(s.todos))
    for _, todo := range s.todos {
        result = append(result, todo)
    }
    return result
}

2. ロックの粒度

// 悪い例: 粗粒度ロック
func (s *TodoStore) Update(id int, req UpdateTodoRequest) (*Todo, error) {
    s.mu.Lock()
    defer s.mu.Unlock()

    // 長時間の処理(検証など)
    if err := req.Validate(); err != nil {
        return nil, err
    }

    todo := s.todos[id]
    // 更新処理
    return todo, nil
}

// 良い例: 細粒度ロック
func (s *TodoStore) Update(id int, req UpdateTodoRequest) (*Todo, error) {
    // ロック外で検証
    if err := req.Validate(); err != nil {
        return nil, err
    }

    s.mu.Lock()
    defer s.mu.Unlock()

    todo := s.todos[id]
    // 必要最小限の処理
    return todo, nil
}

---

プロダクション化への道筋

1. データ永続化

// インターフェース定義
type TodoRepository interface {
    Create(todo Todo) error
    Get(id int) (*Todo, error)
    List() ([]*Todo, error)
    Update(todo Todo) error
    Delete(id int) error
}

// メモリ実装
type InMemoryRepository struct {
    mu    sync.RWMutex
    todos map[int]*Todo
}

// データベース実装
type PostgresRepository struct {
    db *sql.DB
}

2. 認証・認可

// JWTミドルウェア
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")

        claims, err := validateJWT(token)
        if err != nil {
            respondError(w, http.StatusUnauthorized, "invalid token")
            return
        }

        // コンテキストにユーザー情報を保存
        ctx := context.WithValue(r.Context(), "user", claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

3. 構成管理

type Config struct {
    Port         string
    DatabaseURL  string
    JWTSecret    string
    LogLevel     string
}

func LoadConfig() (*Config, error) {
    return &Config{
        Port:        getEnv("PORT", "8080"),
        DatabaseURL: getEnv("DATABASE_URL", ""),
        JWTSecret:   getEnv("JWT_SECRET", ""),
        LogLevel:    getEnv("LOG_LEVEL", "info"),
    }, nil
}

4. ロギング

import "github.com/rs/zerolog/log"

log.Info().
    Str("method", r.Method).
    Str("path", r.URL.Path).
    Int("status", status).
    Dur("duration", duration).
    Msg("request completed")

---

次のステップ

Day 7の実践プロジェクトを通じて、Goでの実用的なWebアプリケーション開発の基礎を習得しました。

さらなる発展:

  • フレームワークの学習: Gin, Echo, Chi
  • データベース統合: sqlx, GORM
  • gRPC: マイクロサービス通信
  • Kubernetes: コンテナオーケストレーション
  • observability: メトリクス、ログ、トレース

Day 8では、これらの概念を総復習し、Go Electivesへの準備を整えます。