Day 7: 実践プロジェクト - 講義

今日の目標

Day 7では、これまでの6日間で学んだ全ての知識を統合して、プロダクションレベルの実践的なWebアプリケーションを構築します。RESTful API、データベース統合、ミドルウェア、テスト、デプロイメントまで、実世界のプロジェクトで必要とされる全ての要素をカバーします。

学習目標

  • HTTPサーバーとRESTful APIの設計と実装
  • データベース統合(SQLiteとPostgreSQL)
  • ミドルウェアパターンの実装
  • 包括的なテスト戦略
  • ロギング、監視、メトリクス
  • グレースフルシャットダウン
  • プロダクションデプロイメント
  • ---

    Part 1: HTTPサーバーとRESTful API

    1.1 標準ライブラリのnet/http

    Goのnet/httpパッケージは非常に強力で、多くの場合フレームワーク不要で本格的なWebサーバーを構築できます。

    package main
    
    import (
        "encoding/json"
        "log"
        "net/http"
    )
    
    // ハンドラー関数の基本
    func helloHandler(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(map[string]string{
            "message": "Hello, World!",
        })
    }
    
    func main() {
        // ルーティング
        http.HandleFunc("/hello", helloHandler)
        http.HandleFunc("/health", healthHandler)
    
        // サーバー起動
        log.Println("Server starting on :8080")
        if err := http.ListenAndServe(":8080", nil); err != nil {
            log.Fatal(err)
        }
    }
    

    1.2 カスタムルーター の実装

    標準ライブラリのhttp.ServeMuxは基本的ですが、本格的なアプリケーションには独自のルーターが必要です。

    package main
    
    import (
        "fmt"
        "net/http"
        "regexp"
        "strings"
    )
    
    // Route represents a single route
    type Route struct {
        Method  string
        Pattern *regexp.Regexp
        Handler http.HandlerFunc
    }
    
    // Router manages routes
    type Router struct {
        routes []Route
    }
    
    // NewRouter creates a new router
    func NewRouter() *Router {
        return &Router{
            routes: make([]Route, 0),
        }
    }
    
    // Handle registers a new route
    func (router *Router) Handle(method, pattern string, handler http.HandlerFunc) {
        regex := regexp.MustCompile("^" + pattern + "$")
        route := Route{
            Method:  method,
            Pattern: regex,
            Handler: handler,
        }
        router.routes = append(router.routes, route)
    }
    
    // GET registers a GET route
    func (router *Router) GET(pattern string, handler http.HandlerFunc) {
        router.Handle("GET", pattern, handler)
    }
    
    // POST registers a POST route
    func (router *Router) POST(pattern string, handler http.HandlerFunc) {
        router.Handle("POST", pattern, handler)
    }
    
    // PUT registers a PUT route
    func (router *Router) PUT(pattern string, handler http.HandlerFunc) {
        router.Handle("PUT", pattern, handler)
    }
    
    // DELETE registers a DELETE route
    func (router *Router) DELETE(pattern string, handler http.HandlerFunc) {
        router.Handle("DELETE", pattern, handler)
    }
    
    // ServeHTTP implements http.Handler
    func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        for _, route := range router.routes {
            if route.Method == r.Method && route.Pattern.MatchString(r.URL.Path) {
                route.Handler(w, r)
                return
            }
        }
    
        // ルートが見つからない
        http.NotFound(w, r)
    }
    
    // 使用例
    func main() {
        router := NewRouter()
    
        router.GET("/users", listUsers)
        router.GET("/users/([0-9]+)", getUser)
        router.POST("/users", createUser)
        router.PUT("/users/([0-9]+)", updateUser)
        router.DELETE("/users/([0-9]+)", deleteUser)
    
        http.ListenAndServe(":8080", router)
    }
    

    1.3 RESTful APIの設計原則

    package api
    
    import (
        "encoding/json"
        "net/http"
        "strconv"
        "strings"
    )
    
    // User represents a user
    type User struct {
        ID        int    `json:"id"`
        Name      string `json:"name"`
        Email     string `json:"email"`
        CreatedAt string `json:"created_at"`
    }
    
    // UserAPI handles user-related endpoints
    type UserAPI struct {
        store UserStore
    }
    
    // NewUserAPI creates a new user API
    func NewUserAPI(store UserStore) *UserAPI {
        return &UserAPI{store: store}
    }
    
    // List handles GET /users
    func (api *UserAPI) List(w http.ResponseWriter, r *http.Request) {
        // クエリパラメータの取得
        page := r.URL.Query().Get("page")
        limit := r.URL.Query().Get("limit")
    
        pageNum, _ := strconv.Atoi(page)
        limitNum, _ := strconv.Atoi(limit)
    
        if pageNum <= 0 {
            pageNum = 1
        }
        if limitNum <= 0 {
            limitNum = 10
        }
    
        // データを取得
        users, err := api.store.List(r.Context(), pageNum, limitNum)
        if err != nil {
            respondError(w, http.StatusInternalServerError, err.Error())
            return
        }
    
        // レスポンス
        respondJSON(w, http.StatusOK, map[string]interface{}{
            "users": users,
            "page":  pageNum,
            "limit": limitNum,
        })
    }
    
    // Get handles GET /users/:id
    func (api *UserAPI) Get(w http.ResponseWriter, r *http.Request) {
        // パスパラメータの取得
        id := extractID(r.URL.Path)
    
        // データを取得
        user, err := api.store.Get(r.Context(), id)
        if err != nil {
            if err == ErrNotFound {
                respondError(w, http.StatusNotFound, "User not found")
            } else {
                respondError(w, http.StatusInternalServerError, err.Error())
            }
            return
        }
    
        respondJSON(w, http.StatusOK, user)
    }
    
    // Create handles POST /users
    func (api *UserAPI) Create(w http.ResponseWriter, r *http.Request) {
        // リクエストボディをデコード
        var input struct {
            Name  string `json:"name"`
            Email string `json:"email"`
        }
    
        if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
            respondError(w, http.StatusBadRequest, "Invalid request body")
            return
        }
    
        // バリデーション
        if input.Name == "" {
            respondError(w, http.StatusBadRequest, "Name is required")
            return
        }
        if input.Email == "" {
            respondError(w, http.StatusBadRequest, "Email is required")
            return
        }
    
        // ユーザーを作成
        user, err := api.store.Create(r.Context(), input.Name, input.Email)
        if err != nil {
            respondError(w, http.StatusInternalServerError, err.Error())
            return
        }
    
        respondJSON(w, http.StatusCreated, user)
    }
    
    // Update handles PUT /users/:id
    func (api *UserAPI) Update(w http.ResponseWriter, r *http.Request) {
        id := extractID(r.URL.Path)
    
        var input struct {
            Name  string `json:"name"`
            Email string `json:"email"`
        }
    
        if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
            respondError(w, http.StatusBadRequest, "Invalid request body")
            return
        }
    
        user, err := api.store.Update(r.Context(), id, input.Name, input.Email)
        if err != nil {
            if err == ErrNotFound {
                respondError(w, http.StatusNotFound, "User not found")
            } else {
                respondError(w, http.StatusInternalServerError, err.Error())
            }
            return
        }
    
        respondJSON(w, http.StatusOK, user)
    }
    
    // Delete handles DELETE /users/:id
    func (api *UserAPI) Delete(w http.ResponseWriter, r *http.Request) {
        id := extractID(r.URL.Path)
    
        if err := api.store.Delete(r.Context(), id); err != nil {
            if err == ErrNotFound {
                respondError(w, http.StatusNotFound, "User not found")
            } else {
                respondError(w, http.StatusInternalServerError, err.Error())
            }
            return
        }
    
        w.WriteHeader(http.StatusNoContent)
    }
    
    // ヘルパー関数
    
    func respondJSON(w http.ResponseWriter, status int, data interface{}) {
        w.Header().Set("Content-Type", "application/json")
        w.WriteHeader(status)
        json.NewEncoder(w).Encode(data)
    }
    
    func respondError(w http.ResponseWriter, status int, message string) {
        respondJSON(w, status, map[string]string{
            "error": message,
        })
    }
    
    func extractID(path string) int {
        parts := strings.Split(path, "/")
        if len(parts) > 0 {
            id, _ := strconv.Atoi(parts[len(parts)-1])
            return id
        }
        return 0
    }
    

    ---

    Part 2: データベース統合

    2.1 database/sql パッケージ

    Goの標準ライブラリdatabase/sqlは、データベース操作のための統一的なインターフェースを提供します。

    package database
    
    import (
        "context"
        "database/sql"
        "fmt"
        "time"
    
        _ "github.com/mattn/go-sqlite3"
    )
    
    // Database wraps a database connection
    type Database struct {
        db *sql.DB
    }
    
    // NewDatabase creates a new database connection
    func NewDatabase(driver, dsn string) (*Database, error) {
        db, err := sql.Open(driver, dsn)
        if err != nil {
            return nil, fmt.Errorf("failed to open database: %w", err)
        }
    
        // 接続プールの設定
        db.SetMaxOpenConns(25)
        db.SetMaxIdleConns(5)
        db.SetConnMaxLifetime(5 * time.Minute)
    
        // 接続確認
        if err := db.Ping(); err != nil {
            return nil, fmt.Errorf("failed to ping database: %w", err)
        }
    
        return &Database{db: db}, nil
    }
    
    // Close closes the database connection
    func (d *Database) Close() error {
        return d.db.Close()
    }
    
    // UserStore implements user storage
    type UserStore struct {
        db *Database
    }
    
    // NewUserStore creates a new user store
    func NewUserStore(db *Database) *UserStore {
        return &UserStore{db: db}
    }
    
    // Create creates a new user
    func (s *UserStore) Create(ctx context.Context, name, email string) (*User, error) {
        query := `
            INSERT INTO users (name, email, created_at)
            VALUES (?, ?, ?)
            RETURNING id, name, email, created_at
        `
    
        var user User
        err := s.db.db.QueryRowContext(
            ctx,
            query,
            name,
            email,
            time.Now(),
        ).Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt)
    
        if err != nil {
            return nil, fmt.Errorf("failed to create user: %w", err)
        }
    
        return &user, nil
    }
    
    // Get retrieves a user by ID
    func (s *UserStore) Get(ctx context.Context, id int) (*User, error) {
        query := `
            SELECT id, name, email, created_at
            FROM users
            WHERE id = ?
        `
    
        var user User
        err := s.db.db.QueryRowContext(ctx, query, id).Scan(
            &user.ID,
            &user.Name,
            &user.Email,
            &user.CreatedAt,
        )
    
        if err == sql.ErrNoRows {
            return nil, ErrNotFound
        }
        if err != nil {
            return nil, fmt.Errorf("failed to get user: %w", err)
        }
    
        return &user, nil
    }
    
    // List retrieves a list of users
    func (s *UserStore) List(ctx context.Context, page, limit int) ([]User, error) {
        offset := (page - 1) * limit
    
        query := `
            SELECT id, name, email, created_at
            FROM users
            ORDER BY created_at DESC
            LIMIT ? OFFSET ?
        `
    
        rows, err := s.db.db.QueryContext(ctx, query, limit, offset)
        if err != nil {
            return nil, fmt.Errorf("failed to list users: %w", err)
        }
        defer rows.Close()
    
        var users []User
        for rows.Next() {
            var user User
            if err := rows.Scan(&user.ID, &user.Name, &user.Email, &user.CreatedAt); err != nil {
                return nil, fmt.Errorf("failed to scan user: %w", err)
            }
            users = append(users, user)
        }
    
        if err := rows.Err(); err != nil {
            return nil, fmt.Errorf("error iterating users: %w", err)
        }
    
        return users, nil
    }
    
    // Update updates a user
    func (s *UserStore) Update(ctx context.Context, id int, name, email string) (*User, error) {
        query := `
            UPDATE users
            SET name = ?, email = ?
            WHERE id = ?
            RETURNING id, name, email, created_at
        `
    
        var user User
        err := s.db.db.QueryRowContext(ctx, query, name, email, id).Scan(
            &user.ID,
            &user.Name,
            &user.Email,
            &user.CreatedAt,
        )
    
        if err == sql.ErrNoRows {
            return nil, ErrNotFound
        }
        if err != nil {
            return nil, fmt.Errorf("failed to update user: %w", err)
        }
    
        return &user, nil
    }
    
    // Delete deletes a user
    func (s *UserStore) Delete(ctx context.Context, id int) error {
        query := `DELETE FROM users WHERE id = ?`
    
        result, err := s.db.db.ExecContext(ctx, query, id)
        if err != nil {
            return fmt.Errorf("failed to delete user: %w", err)
        }
    
        rows, err := result.RowsAffected()
        if err != nil {
            return fmt.Errorf("failed to get affected rows: %w", err)
        }
    
        if rows == 0 {
            return ErrNotFound
        }
    
        return nil
    }
    

    2.2 トランザクション処理

    package database
    
    import (
        "context"
        "database/sql"
        "fmt"
    )
    
    // WithTransaction executes a function within a transaction
    func (d *Database) WithTransaction(ctx context.Context, fn func(*sql.Tx) error) error {
        tx, err := d.db.BeginTx(ctx, nil)
        if err != nil {
            return fmt.Errorf("failed to begin transaction: %w", err)
        }
    
        defer func() {
            if p := recover(); p != nil {
                tx.Rollback()
                panic(p)
            }
        }()
    
        if err := fn(tx); err != nil {
            if rbErr := tx.Rollback(); rbErr != nil {
                return fmt.Errorf("tx error: %v, rb error: %v", err, rbErr)
            }
            return err
        }
    
        if err := tx.Commit(); err != nil {
            return fmt.Errorf("failed to commit transaction: %w", err)
        }
    
        return nil
    }
    
    // 使用例: 複数のユーザーを一括作成
    func (s *UserStore) CreateBatch(ctx context.Context, users []CreateUserInput) error {
        return s.db.WithTransaction(ctx, func(tx *sql.Tx) error {
            stmt, err := tx.PrepareContext(ctx, `
                INSERT INTO users (name, email, created_at)
                VALUES (?, ?, ?)
            `)
            if err != nil {
                return err
            }
            defer stmt.Close()
    
            for _, user := range users {
                _, err := stmt.ExecContext(ctx, user.Name, user.Email, time.Now())
                if err != nil {
                    return fmt.Errorf("failed to insert user %s: %w", user.Name, err)
                }
            }
    
            return nil
        })
    }
    

    2.3 マイグレーション

    package migration
    
    import (
        "database/sql"
        "fmt"
    )
    
    // Migration represents a database migration
    type Migration struct {
        Version int
        Name    string
        Up      func(*sql.DB) error
        Down    func(*sql.DB) error
    }
    
    // Migrator manages database migrations
    type Migrator struct {
        db         *sql.DB
        migrations []Migration
    }
    
    // NewMigrator creates a new migrator
    func NewMigrator(db *sql.DB) *Migrator {
        return &Migrator{
            db:         db,
            migrations: make([]Migration, 0),
        }
    }
    
    // Add adds a migration
    func (m *Migrator) Add(migration Migration) {
        m.migrations = append(m.migrations, migration)
    }
    
    // Up runs all pending migrations
    func (m *Migrator) Up() error {
        // マイグレーションテーブルを作成
        if err := m.createMigrationTable(); err != nil {
            return err
        }
    
        // 現在のバージョンを取得
        current, err := m.getCurrentVersion()
        if err != nil {
            return err
        }
    
        // ペンディング中のマイグレーションを実行
        for _, migration := range m.migrations {
            if migration.Version <= current {
                continue
            }
    
            fmt.Printf("Running migration %d: %s\n", migration.Version, migration.Name)
    
            if err := migration.Up(m.db); err != nil {
                return fmt.Errorf("migration %d failed: %w", migration.Version, err)
            }
    
            if err := m.setVersion(migration.Version); err != nil {
                return err
            }
        }
    
        return nil
    }
    
    func (m *Migrator) createMigrationTable() error {
        query := `
            CREATE TABLE IF NOT EXISTS migrations (
                version INTEGER PRIMARY KEY,
                applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        `
        _, err := m.db.Exec(query)
        return err
    }
    
    func (m *Migrator) getCurrentVersion() (int, error) {
        var version int
        err := m.db.QueryRow(`
            SELECT COALESCE(MAX(version), 0) FROM migrations
        `).Scan(&version)
        return version, err
    }
    
    func (m *Migrator) setVersion(version int) error {
        _, err := m.db.Exec(`
            INSERT INTO migrations (version) VALUES (?)
        `, version)
        return err
    }
    
    // マイグレーション定義例
    var migrations = []Migration{
        {
            Version: 1,
            Name:    "create_users_table",
            Up: func(db *sql.DB) error {
                _, err := db.Exec(`
                    CREATE TABLE users (
                        id INTEGER PRIMARY KEY AUTOINCREMENT,
                        name TEXT NOT NULL,
                        email TEXT UNIQUE NOT NULL,
                        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                    )
                `)
                return err
            },
            Down: func(db *sql.DB) error {
                _, err := db.Exec(`DROP TABLE users`)
                return err
            },
        },
        {
            Version: 2,
            Name:    "add_users_updated_at",
            Up: func(db *sql.DB) error {
                _, err := db.Exec(`
                    ALTER TABLE users ADD COLUMN updated_at TIMESTAMP
                `)
                return err
            },
            Down: func(db *sql.DB) error {
                // SQLiteではALTER TABLE DROP COLUMNがサポートされていない
                return nil
            },
        },
    }
    

    ---

    Part 3: ミドルウェアパターン

    3.1 ミドルウェアの基本

    package middleware
    
    import (
        "log"
        "net/http"
        "time"
    )
    
    // Middleware represents a middleware function
    type Middleware func(http.Handler) http.Handler
    
    // Logger logs HTTP requests
    func Logger(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),
            )
        })
    }
    
    type responseWriter struct {
        http.ResponseWriter
        statusCode int
    }
    
    func (rw *responseWriter) WriteHeader(code int) {
        rw.statusCode = code
        rw.ResponseWriter.WriteHeader(code)
    }
    
    // Recovery recovers from panics
    func Recovery(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 recovered: %v", err)
                    http.Error(w, "Internal Server Error", http.StatusInternalServerError)
                }
            }()
    
            next.ServeHTTP(w, r)
        })
    }
    
    // CORS adds CORS headers
    func CORS(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            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, Authorization")
    
            if r.Method == "OPTIONS" {
                w.WriteHeader(http.StatusOK)
                return
            }
    
            next.ServeHTTP(w, r)
        })
    }
    
    // RequestID adds a unique request ID
    func RequestID(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            requestID := generateRequestID()
            ctx := context.WithValue(r.Context(), requestIDKey, requestID)
            w.Header().Set("X-Request-ID", requestID)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
    
    // Timeout adds a timeout to requests
    func Timeout(timeout time.Duration) Middleware {
        return func(next http.Handler) http.Handler {
            return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                ctx, cancel := context.WithTimeout(r.Context(), timeout)
                defer cancel()
    
                done := make(chan struct{})
    
                go func() {
                    next.ServeHTTP(w, r.WithContext(ctx))
                    close(done)
                }()
    
                select {
                case <-done:
                    return
                case <-ctx.Done():
                    http.Error(w, "Request timeout", http.StatusGatewayTimeout)
                }
            })
        }
    }
    
    // RateLimit implements rate limiting
    func RateLimit(requestsPerSecond int) Middleware {
        limiter := rate.NewLimiter(rate.Limit(requestsPerSecond), requestsPerSecond)
    
        return func(next http.Handler) http.Handler {
            return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                if !limiter.Allow() {
                    http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
                    return
                }
    
                next.ServeHTTP(w, r)
            })
        }
    }
    
    // Chain chains multiple middlewares
    func Chain(middlewares ...Middleware) Middleware {
        return func(final http.Handler) http.Handler {
            for i := len(middlewares) - 1; i >= 0; i-- {
                final = middlewares[i](final)
            }
            return final
        }
    }
    

    3.2 認証ミドルウェア

    package middleware
    
    import (
        "context"
        "net/http"
        "strings"
    )
    
    type contextKey string
    
    const userContextKey contextKey = "user"
    
    // Auth implements JWT authentication
    type Auth struct {
        jwtSecret string
    }
    
    // NewAuth creates a new auth middleware
    func NewAuth(jwtSecret string) *Auth {
        return &Auth{jwtSecret: jwtSecret}
    }
    
    // Middleware returns the authentication middleware
    func (a *Auth) Middleware(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // トークンを取得
            token := extractToken(r)
            if token == "" {
                http.Error(w, "Unauthorized", http.StatusUnauthorized)
                return
            }
    
            // トークンを検証
            claims, err := validateToken(token, a.jwtSecret)
            if err != nil {
                http.Error(w, "Invalid token", http.StatusUnauthorized)
                return
            }
    
            // ユーザー情報をcontextに保存
            ctx := context.WithValue(r.Context(), userContextKey, claims)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
    
    func extractToken(r *http.Request) string {
        bearerToken := r.Header.Get("Authorization")
        if bearerToken == "" {
            return ""
        }
    
        parts := strings.Split(bearerToken, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            return ""
        }
    
        return parts[1]
    }
    
    // GetUserFromContext retrieves user from context
    func GetUserFromContext(ctx context.Context) (*User, bool) {
        user, ok := ctx.Value(userContextKey).(*User)
        return user, ok
    }
    

    ---

    Part 4: テスト戦略

    4.1 ユニットテスト

    package api_test
    
    import (
        "bytes"
        "encoding/json"
        "net/http"
        "net/http/httptest"
        "testing"
    
        "yourapp/api"
    )
    
    func TestUserAPI_Create(t *testing.T) {
        // モックストアを作成
        store := &MockUserStore{}
        userAPI := api.NewUserAPI(store)
    
        // テストケース
        tests := []struct {
            name           string
            input          map[string]string
            expectedStatus int
            setupMock      func(*MockUserStore)
        }{
            {
                name: "valid user",
                input: map[string]string{
                    "name":  "John Doe",
                    "email": "john@example.com",
                },
                expectedStatus: http.StatusCreated,
                setupMock: func(m *MockUserStore) {
                    m.CreateFunc = func(ctx context.Context, name, email string) (*User, error) {
                        return &User{ID: 1, Name: name, Email: email}, nil
                    }
                },
            },
            {
                name: "missing name",
                input: map[string]string{
                    "email": "john@example.com",
                },
                expectedStatus: http.StatusBadRequest,
                setupMock:      func(m *MockUserStore) {},
            },
        }
    
        for _, tt := range tests {
            t.Run(tt.name, func(t *testing.T) {
                tt.setupMock(store)
    
                // リクエストを作成
                body, _ := json.Marshal(tt.input)
                req := httptest.NewRequest("POST", "/users", bytes.NewReader(body))
                rec := httptest.NewRecorder()
    
                // ハンドラを実行
                userAPI.Create(rec, req)
    
                // ステータスコードを確認
                if rec.Code != tt.expectedStatus {
                    t.Errorf("expected status %d, got %d", tt.expectedStatus, rec.Code)
                }
            })
        }
    }
    

    4.2 統合テスト

    package integration_test
    
    import (
        "context"
        "database/sql"
        "testing"
    
        _ "github.com/mattn/go-sqlite3"
        "yourapp/database"
    )
    
    func setupTestDB(t *testing.T) *database.Database {
        db, err := database.NewDatabase("sqlite3", ":memory:")
        if err != nil {
            t.Fatal(err)
        }
    
        // マイグレーションを実行
        migrator := migration.NewMigrator(db.DB())
        for _, m := range migrations {
            migrator.Add(m)
        }
        if err := migrator.Up(); err != nil {
            t.Fatal(err)
        }
    
        return db
    }
    
    func TestUserStore_Integration(t *testing.T) {
        db := setupTestDB(t)
        defer db.Close()
    
        store := database.NewUserStore(db)
        ctx := context.Background()
    
        // Create
        user, err := store.Create(ctx, "John Doe", "john@example.com")
        if err != nil {
            t.Fatalf("failed to create user: %v", err)
        }
    
        // Get
        retrieved, err := store.Get(ctx, user.ID)
        if err != nil {
            t.Fatalf("failed to get user: %v", err)
        }
    
        if retrieved.Name != user.Name {
            t.Errorf("expected name %s, got %s", user.Name, retrieved.Name)
        }
    
        // Update
        updated, err := store.Update(ctx, user.ID, "Jane Doe", "jane@example.com")
        if err != nil {
            t.Fatalf("failed to update user: %v", err)
        }
    
        if updated.Name != "Jane Doe" {
            t.Errorf("expected name Jane Doe, got %s", updated.Name)
        }
    
        // Delete
        if err := store.Delete(ctx, user.ID); err != nil {
            t.Fatalf("failed to delete user: %v", err)
        }
    
        // 削除確認
        _, err = store.Get(ctx, user.ID)
        if err != database.ErrNotFound {
            t.Errorf("expected ErrNotFound, got %v", err)
        }
    }
    

    ---

    Part 5: ロギングと監視

    5.1 構造化ロギング

    package logging
    
    import (
        "context"
        "io"
        "log/slog"
        "os"
    )
    
    // Logger wraps slog.Logger
    type Logger struct {
        *slog.Logger
    }
    
    // NewLogger creates a new logger
    func NewLogger(output io.Writer) *Logger {
        handler := slog.NewJSONHandler(output, &slog.HandlerOptions{
            Level: slog.LevelInfo,
        })
    
        return &Logger{
            Logger: slog.New(handler),
        }
    }
    
    // WithContext adds context to logger
    func (l *Logger) WithContext(ctx context.Context) *Logger {
        // Request IDなどをcontextから取得
        if requestID, ok := ctx.Value(requestIDKey).(string); ok {
            return &Logger{
                Logger: l.With("request_id", requestID),
            }
        }
    
        return l
    }
    
    // 使用例
    func handleRequest(w http.ResponseWriter, r *http.Request) {
        logger := GetLogger().WithContext(r.Context())
    
        logger.Info("handling request",
            "method", r.Method,
            "path", r.URL.Path,
        )
    
        // 処理...
    
        logger.Info("request completed",
            "status", 200,
            "duration_ms", 42,
        )
    }
    

    5.2 メトリクス収集

    package metrics
    
    import (
        "net/http"
        "time"
    
        "github.com/prometheus/client_golang/prometheus"
        "github.com/prometheus/client_golang/prometheus/promauto"
        "github.com/prometheus/client_golang/prometheus/promhttp"
    )
    
    var (
        httpRequestsTotal = promauto.NewCounterVec(
            prometheus.CounterOpts{
                Name: "http_requests_total",
                Help: "Total number of HTTP requests",
            },
            []string{"method", "endpoint", "status"},
        )
    
        httpRequestDuration = promauto.NewHistogramVec(
            prometheus.HistogramOpts{
                Name:    "http_request_duration_seconds",
                Help:    "HTTP request duration in seconds",
                Buckets: prometheus.DefBuckets,
            },
            []string{"method", "endpoint"},
        )
    )
    
    // MetricsMiddleware records metrics
    func MetricsMiddleware(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()
            wrapped := &responseWriter{ResponseWriter: w, statusCode: 200}
    
            next.ServeHTTP(wrapped, r)
    
            duration := time.Since(start).Seconds()
    
            httpRequestsTotal.WithLabelValues(
                r.Method,
                r.URL.Path,
                http.StatusText(wrapped.statusCode),
            ).Inc()
    
            httpRequestDuration.WithLabelValues(
                r.Method,
                r.URL.Path,
            ).Observe(duration)
        })
    }
    
    // メトリクスエンドポイント
    func MetricsHandler() http.Handler {
        return promhttp.Handler()
    }
    

    ---

    Part 6: グレースフルシャットダウン

    package server
    
    import (
        "context"
        "log"
        "net/http"
        "os"
        "os/signal"
        "syscall"
        "time"
    )
    
    // Server represents the HTTP server
    type Server struct {
        httpServer *http.Server
        logger     *log.Logger
    }
    
    // NewServer creates a new server
    func NewServer(addr string, handler http.Handler) *Server {
        return &Server{
            httpServer: &http.Server{
                Addr:         addr,
                Handler:      handler,
                ReadTimeout:  15 * time.Second,
                WriteTimeout: 15 * time.Second,
                IdleTimeout:  60 * time.Second,
            },
            logger: log.New(os.Stdout, "[SERVER] ", log.LstdFlags),
        }
    }
    
    // Start starts the server
    func (s *Server) Start() error {
        // シグナルハンドラを設定
        stop := make(chan os.Signal, 1)
        signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
    
        // サーバーを起動
        go func() {
            s.logger.Printf("Server starting on %s", s.httpServer.Addr)
            if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
                s.logger.Fatalf("Server failed: %v", err)
            }
        }()
    
        // シグナルを待つ
        <-stop
    
        // グレースフルシャットダウン
        s.logger.Println("Shutting down server...")
    
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()
    
        if err := s.httpServer.Shutdown(ctx); err != nil {
            s.logger.Printf("Server shutdown error: %v", err)
            return err
        }
    
        s.logger.Println("Server stopped")
        return nil
    }
    

    ---

    まとめ

    Day 7では、プロダクションレベルのWebアプリケーションを構築するための実践的な知識を学びました:

  • HTTPサーバー: ルーティング、RESTful API設計
  • データベース: SQL操作、トランザクション、マイグレーション
  • ミドルウェア: ロギング、認証、レート制限
  • テスト: ユニットテスト、統合テスト
  • 監視: ロギング、メトリクス
  • デプロイ: グレースフルシャットダウン

これらの知識は、実世界のGoアプリケーション開発で直接使用できます。Day 8では、最終プロジェクトとしてこれらを全て統合した完全なアプリケーションを構築します。