第20章: 実践プロジェクト

はじめに

この章では、これまで学んだすべての知識を統合して、実用的な本番環境レベルのアプリケーションを構築します。プロジェクト構成、設計パターン、エラーハンドリング、ロギング、テスト、監視、グレースフルシャットダウン、配布方法など、実際の開発で必要となるすべての要素を網羅します。

プロダクションレディなアーキテクチャ

標準的なプロジェクト構造

myapp/
├── cmd/
│   └── myapp/
│       └── main.go              # エントリーポイント
├── internal/
│   ├── server/                  # HTTPサーバー実装
│   │   ├── server.go
│   │   ├── handlers.go
│   │   ├── middleware.go
│   │   └── routes.go
│   ├── service/                 # ビジネスロジック
│   │   └── service.go
│   ├── repository/              # データアクセス層
│   │   └── repository.go
│   ├── config/                  # 設定管理
│   │   └── config.go
│   └── logger/                  # ログ管理
│       └── logger.go
├── pkg/                         # 外部公開可能なパッケージ
│   └── models/
│       └── models.go
├── test/
│   ├── integration/
│   └── testdata/
├── deployments/
│   ├── Dockerfile
│   └── k8s/
├── scripts/
│   └── build.sh
├── go.mod
├── go.sum
├── Makefile
├── README.md
└── .env.example

🔑 重要: この構造は、スケーラビリティとメンテナンス性を考慮した業界標準です。

レイヤードアーキテクチャ

┌─────────────────────────────────────┐
│  Presentation Layer (HTTP)          │
│  - ハンドラー                        │
│  - ミドルウェア                      │
│  - ルーティング                      │
└──────────────┬──────────────────────┘
               │
               v
┌─────────────────────────────────────┐
│  Service Layer (Business Logic)     │
│  - ビジネスルール                    │
│  - トランザクション管理              │
│  - 外部API呼び出し                   │
└──────────────┬──────────────────────┘
               │
               v
┌─────────────────────────────────────┐
│  Repository Layer (Data Access)     │
│  - データベースクエリ                │
│  - キャッシュ                        │
│  - データマッピング                  │
└─────────────────────────────────────┘

💡 設計原則: 各レイヤーは上位レイヤーのみに依存し、下位レイヤーには依存しません(依存性逆転の原則)。

設定管理システム

環境ごとの設定

// internal/config/config.go
package config

import (
    "fmt"
    "os"
    "strconv"
    "time"
)

type Config struct {
    Server   ServerConfig
    Database DatabaseConfig
    Redis    RedisConfig
    Logger   LoggerConfig
}

type ServerConfig struct {
    Host         string
    Port         int
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
    IdleTimeout  time.Duration
}

type DatabaseConfig struct {
    Host            string
    Port            int
    User            string
    Password        string
    Database        string
    MaxOpenConns    int
    MaxIdleConns    int
    ConnMaxLifetime time.Duration
}

type RedisConfig struct {
    Host     string
    Port     int
    Password string
    DB       int
}

type LoggerConfig struct {
    Level  string
    Format string
}

// Load は環境変数から設定を読み込む
func Load() (*Config, error) {
    cfg := &Config{
        Server: ServerConfig{
            Host:         getEnv("SERVER_HOST", "0.0.0.0"),
            Port:         getEnvInt("SERVER_PORT", 8080),
            ReadTimeout:  getEnvDuration("SERVER_READ_TIMEOUT", 15*time.Second),
            WriteTimeout: getEnvDuration("SERVER_WRITE_TIMEOUT", 15*time.Second),
            IdleTimeout:  getEnvDuration("SERVER_IDLE_TIMEOUT", 60*time.Second),
        },
        Database: DatabaseConfig{
            Host:            getEnv("DB_HOST", "localhost"),
            Port:            getEnvInt("DB_PORT", 5432),
            User:            getEnv("DB_USER", "postgres"),
            Password:        getEnv("DB_PASSWORD", ""),
            Database:        getEnv("DB_NAME", "myapp"),
            MaxOpenConns:    getEnvInt("DB_MAX_OPEN_CONNS", 25),
            MaxIdleConns:    getEnvInt("DB_MAX_IDLE_CONNS", 5),
            ConnMaxLifetime: getEnvDuration("DB_CONN_MAX_LIFETIME", 5*time.Minute),
        },
        Redis: RedisConfig{
            Host:     getEnv("REDIS_HOST", "localhost"),
            Port:     getEnvInt("REDIS_PORT", 6379),
            Password: getEnv("REDIS_PASSWORD", ""),
            DB:       getEnvInt("REDIS_DB", 0),
        },
        Logger: LoggerConfig{
            Level:  getEnv("LOG_LEVEL", "info"),
            Format: getEnv("LOG_FORMAT", "json"),
        },
    }

    // 検証
    if err := cfg.Validate(); err != nil {
        return nil, fmt.Errorf("設定検証失敗: %w", err)
    }

    return cfg, nil
}

func (c *Config) Validate() error {
    if c.Server.Port < 1 || c.Server.Port > 65535 {
        return fmt.Errorf("無効なポート番号: %d", c.Server.Port)
    }
    if c.Database.Password == "" {
        return fmt.Errorf("データベースパスワードが設定されていません")
    }
    return nil
}

// ヘルパー関数
func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

func getEnvInt(key string, defaultValue int) int {
    if value := os.Getenv(key); value != "" {
        if intValue, err := strconv.Atoi(value); err == nil {
            return intValue
        }
    }
    return defaultValue
}

func getEnvDuration(key string, defaultValue time.Duration) time.Duration {
    if value := os.Getenv(key); value != "" {
        if duration, err := time.ParseDuration(value); err == nil {
            return duration
        }
    }
    return defaultValue
}

.env ファイルサポート

// 開発環境用の .env ファイル読み込み
func LoadEnvFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := scanner.Text()

        // コメントと空行をスキップ
        if len(line) == 0 || line[0] == '#' {
            continue
        }

        // KEY=VALUE 形式をパース
        parts := strings.SplitN(line, "=", 2)
        if len(parts) != 2 {
            continue
        }

        key := strings.TrimSpace(parts[0])
        value := strings.TrimSpace(parts[1])

        // 引用符を削除
        value = strings.Trim(value, "\"'")

        os.Setenv(key, value)
    }

    return scanner.Err()
}

構造化ロギングシステム

ロガーの実装

// internal/logger/logger.go
package logger

import (
    "context"
    "encoding/json"
    "fmt"
    "io"
    "os"
    "runtime"
    "time"
)

type Level int

const (
    DebugLevel Level = iota
    InfoLevel
    WarnLevel
    ErrorLevel
    FatalLevel
)

var levelNames = map[Level]string{
    DebugLevel: "DEBUG",
    InfoLevel:  "INFO",
    WarnLevel:  "WARN",
    ErrorLevel: "ERROR",
    FatalLevel: "FATAL",
}

type Logger struct {
    out    io.Writer
    level  Level
    format string
}

type Entry struct {
    Time    time.Time              `json:"time"`
    Level   string                 `json:"level"`
    Message string                 `json:"message"`
    Fields  map[string]interface{} `json:"fields,omitempty"`
    Caller  string                 `json:"caller,omitempty"`
}

func New(out io.Writer, level Level, format string) *Logger {
    return &Logger{
        out:    out,
        level:  level,
        format: format,
    }
}

func (l *Logger) log(level Level, msg string, fields map[string]interface{}) {
    if level < l.level {
        return
    }

    entry := Entry{
        Time:    time.Now(),
        Level:   levelNames[level],
        Message: msg,
        Fields:  fields,
    }

    // 呼び出し元情報を追加
    if _, file, line, ok := runtime.Caller(2); ok {
        entry.Caller = fmt.Sprintf("%s:%d", file, line)
    }

    l.write(entry)
}

func (l *Logger) write(entry Entry) {
    var output string

    if l.format == "json" {
        data, _ := json.Marshal(entry)
        output = string(data) + "\n"
    } else {
        // テキスト形式
        output = fmt.Sprintf("[%s] %s: %s",
            entry.Time.Format("2006-01-02 15:04:05"),
            entry.Level,
            entry.Message,
        )
        if len(entry.Fields) > 0 {
            output += fmt.Sprintf(" %+v", entry.Fields)
        }
        output += "\n"
    }

    l.out.Write([]byte(output))
}

func (l *Logger) Debug(msg string, fields ...map[string]interface{}) {
    l.log(DebugLevel, msg, mergeFields(fields...))
}

func (l *Logger) Info(msg string, fields ...map[string]interface{}) {
    l.log(InfoLevel, msg, mergeFields(fields...))
}

func (l *Logger) Warn(msg string, fields ...map[string]interface{}) {
    l.log(WarnLevel, msg, mergeFields(fields...))
}

func (l *Logger) Error(msg string, fields ...map[string]interface{}) {
    l.log(ErrorLevel, msg, mergeFields(fields...))
}

func (l *Logger) Fatal(msg string, fields ...map[string]interface{}) {
    l.log(FatalLevel, msg, mergeFields(fields...))
    os.Exit(1)
}

func mergeFields(fields ...map[string]interface{}) map[string]interface{} {
    result := make(map[string]interface{})
    for _, f := range fields {
        for k, v := range f {
            result[k] = v
        }
    }
    return result
}

// コンテキストからリクエスト情報を取得
func (l *Logger) WithContext(ctx context.Context) *ContextLogger {
    return &ContextLogger{
        logger: l,
        ctx:    ctx,
    }
}

type ContextLogger struct {
    logger *Logger
    ctx    context.Context
}

func (cl *ContextLogger) Info(msg string, fields ...map[string]interface{}) {
    allFields := mergeFields(fields...)

    // コンテキストから情報を抽出
    if requestID, ok := cl.ctx.Value("requestID").(string); ok {
        allFields["request_id"] = requestID
    }
    if userID, ok := cl.ctx.Value("userID").(string); ok {
        allFields["user_id"] = userID
    }

    cl.logger.Info(msg, allFields)
}

ロギングミドルウェア

func LoggingMiddleware(logger *logger.Logger) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            start := time.Now()

            // ResponseWriterをラップ
            rw := &responseWriter{
                ResponseWriter: w,
                statusCode:     http.StatusOK,
            }

            // リクエスト情報をログ
            logger.Info("リクエスト開始", map[string]interface{}{
                "method":     r.Method,
                "path":       r.URL.Path,
                "remote_ip":  r.RemoteAddr,
                "user_agent": r.UserAgent(),
            })

            next.ServeHTTP(rw, r)

            // レスポンス情報をログ
            duration := time.Since(start)
            logger.Info("リクエスト完了", map[string]interface{}{
                "method":      r.Method,
                "path":        r.URL.Path,
                "status_code": rw.statusCode,
                "duration_ms": duration.Milliseconds(),
                "bytes":       rw.written,
            })
        })
    }
}

データベース層の実装

リポジトリパターン

// internal/repository/repository.go
package repository

import (
    "context"
    "database/sql"
    "fmt"
    "time"

    "myapp/pkg/models"
)

type UserRepository interface {
    Create(ctx context.Context, user *models.User) error
    FindByID(ctx context.Context, id int64) (*models.User, error)
    FindByEmail(ctx context.Context, email string) (*models.User, error)
    Update(ctx context.Context, user *models.User) error
    Delete(ctx context.Context, id int64) error
    List(ctx context.Context, limit, offset int) ([]*models.User, error)
}

type userRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) Create(ctx context.Context, user *models.User) error {
    query := `
        INSERT INTO users (email, name, password_hash, created_at, updated_at)
        VALUES ($1, $2, $3, $4, $5)
        RETURNING id
    `

    now := time.Now()
    err := r.db.QueryRowContext(
        ctx,
        query,
        user.Email,
        user.Name,
        user.PasswordHash,
        now,
        now,
    ).Scan(&user.ID)

    if err != nil {
        return fmt.Errorf("ユーザー作成失敗: %w", err)
    }

    user.CreatedAt = now
    user.UpdatedAt = now
    return nil
}

func (r *userRepository) FindByID(ctx context.Context, id int64) (*models.User, error) {
    query := `
        SELECT id, email, name, password_hash, created_at, updated_at
        FROM users
        WHERE id = $1
    `

    user := &models.User{}
    err := r.db.QueryRowContext(ctx, query, id).Scan(
        &user.ID,
        &user.Email,
        &user.Name,
        &user.PasswordHash,
        &user.CreatedAt,
        &user.UpdatedAt,
    )

    if err == sql.ErrNoRows {
        return nil, fmt.Errorf("ユーザーが見つかりません: %d", id)
    }
    if err != nil {
        return nil, fmt.Errorf("ユーザー取得失敗: %w", err)
    }

    return user, nil
}

func (r *userRepository) List(ctx context.Context, limit, offset int) ([]*models.User, error) {
    query := `
        SELECT id, email, name, password_hash, created_at, updated_at
        FROM users
        ORDER BY created_at DESC
        LIMIT $1 OFFSET $2
    `

    rows, err := r.db.QueryContext(ctx, query, limit, offset)
    if err != nil {
        return nil, fmt.Errorf("ユーザー一覧取得失敗: %w", err)
    }
    defer rows.Close()

    var users []*models.User
    for rows.Next() {
        user := &models.User{}
        err := rows.Scan(
            &user.ID,
            &user.Email,
            &user.Name,
            &user.PasswordHash,
            &user.CreatedAt,
            &user.UpdatedAt,
        )
        if err != nil {
            return nil, fmt.Errorf("ユーザースキャン失敗: %w", err)
        }
        users = append(users, user)
    }

    if err := rows.Err(); err != nil {
        return nil, fmt.Errorf("行反復エラー: %w", err)
    }

    return users, nil
}

コネクションプール管理

func InitDB(cfg *config.DatabaseConfig) (*sql.DB, error) {
    dsn := fmt.Sprintf(
        "host=%s port=%d user=%s password=%s dbname=%s sslmode=disable",
        cfg.Host,
        cfg.Port,
        cfg.User,
        cfg.Password,
        cfg.Database,
    )

    db, err := sql.Open("postgres", dsn)
    if err != nil {
        return nil, fmt.Errorf("DB接続失敗: %w", err)
    }

    // コネクションプール設定
    db.SetMaxOpenConns(cfg.MaxOpenConns)
    db.SetMaxIdleConns(cfg.MaxIdleConns)
    db.SetConnMaxLifetime(cfg.ConnMaxLifetime)

    // 接続テスト
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := db.PingContext(ctx); err != nil {
        return nil, fmt.Errorf("DBping失敗: %w", err)
    }

    return db, nil
}

コネクションプールの最適化:

設定パラメータの推奨値:

MaxOpenConns: CPU数 * 2 + ディスクI/O数
  例: 4コアCPU + 1ディスク = 10接続

MaxIdleConns: MaxOpenConnsの25%
  例: MaxOpenConns=10 → MaxIdleConns=2-3

ConnMaxLifetime: 5分
  理由: ロードバランサーのタイムアウトより短く

┌────────────────────────────────────┐
│ コネクションプールの動作            │
├────────────────────────────────────┤
│ リクエスト1 → Conn1 (新規作成)      │
│ リクエスト2 → Conn2 (新規作成)      │
│ リクエスト3 → Conn1 (再利用)        │
│ リクエスト4 → Conn3 (新規作成)      │
│ ...                                │
│ アイドル5分 → Conn1, Conn2 クローズ │
│ リクエストN → Conn3 (再利用)        │
└────────────────────────────────────┘

サービス層(ビジネスロジック)

サービスの実装

// internal/service/user_service.go
package service

import (
    "context"
    "fmt"
    "time"

    "golang.org/x/crypto/bcrypt"
    "myapp/internal/repository"
    "myapp/pkg/models"
)

type UserService interface {
    Register(ctx context.Context, email, name, password string) (*models.User, error)
    Authenticate(ctx context.Context, email, password string) (*models.User, error)
    GetUser(ctx context.Context, id int64) (*models.User, error)
    ListUsers(ctx context.Context, page, pageSize int) ([]*models.User, error)
}

type userService struct {
    repo repository.UserRepository
}

func NewUserService(repo repository.UserRepository) UserService {
    return &userService{repo: repo}
}

func (s *userService) Register(ctx context.Context, email, name, password string) (*models.User, error) {
    // バリデーション
    if err := validateEmail(email); err != nil {
        return nil, fmt.Errorf("無効なメールアドレス: %w", err)
    }
    if len(password) < 8 {
        return nil, fmt.Errorf("パスワードは8文字以上必要です")
    }

    // 重複チェック
    existing, err := s.repo.FindByEmail(ctx, email)
    if err == nil && existing != nil {
        return nil, fmt.Errorf("メールアドレスは既に使用されています")
    }

    // パスワードハッシュ化
    hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        return nil, fmt.Errorf("パスワードハッシュ化失敗: %w", err)
    }

    user := &models.User{
        Email:        email,
        Name:         name,
        PasswordHash: string(hash),
    }

    if err := s.repo.Create(ctx, user); err != nil {
        return nil, fmt.Errorf("ユーザー作成失敗: %w", err)
    }

    return user, nil
}

func (s *userService) Authenticate(ctx context.Context, email, password string) (*models.User, error) {
    user, err := s.repo.FindByEmail(ctx, email)
    if err != nil {
        return nil, fmt.Errorf("認証失敗: ユーザーが見つかりません")
    }

    // パスワード検証
    if err := bcrypt.CompareHashAndPassword(
        []byte(user.PasswordHash),
        []byte(password),
    ); err != nil {
        return nil, fmt.Errorf("認証失敗: パスワードが正しくありません")
    }

    return user, nil
}

トランザクション管理

type Transactor interface {
    WithTransaction(ctx context.Context, fn func(context.Context) error) error
}

type transactor struct {
    db *sql.DB
}

func NewTransactor(db *sql.DB) Transactor {
    return &transactor{db: db}
}

func (t *transactor) WithTransaction(ctx context.Context, fn func(context.Context) error) error {
    tx, err := t.db.BeginTx(ctx, nil)
    if err != nil {
        return fmt.Errorf("トランザクション開始失敗: %w", err)
    }

    // コンテキストにトランザクションを保存
    ctx = context.WithValue(ctx, "tx", tx)

    defer func() {
        if p := recover(); p != nil {
            tx.Rollback()
            panic(p)
        }
    }()

    if err := fn(ctx); err != nil {
        if rbErr := tx.Rollback(); rbErr != nil {
            return fmt.Errorf("ロールバック失敗: %v (元のエラー: %w)", rbErr, err)
        }
        return err
    }

    if err := tx.Commit(); err != nil {
        return fmt.Errorf("コミット失敗: %w", err)
    }

    return nil
}

HTTPサーバーの実装

サーバー初期化

// internal/server/server.go
package server

import (
    "context"
    "fmt"
    "net/http"
    "time"

    "myapp/internal/config"
    "myapp/internal/logger"
    "myapp/internal/service"
)

type Server struct {
    config  *config.ServerConfig
    logger  *logger.Logger
    service service.UserService
    server  *http.Server
}

func New(
    cfg *config.ServerConfig,
    log *logger.Logger,
    svc service.UserService,
) *Server {
    return &Server{
        config:  cfg,
        logger:  log,
        service: svc,
    }
}

func (s *Server) Start() error {
    // ルーター設定
    router := s.setupRoutes()

    // HTTPサーバー設定
    s.server = &http.Server{
        Addr:         fmt.Sprintf("%s:%d", s.config.Host, s.config.Port),
        Handler:      router,
        ReadTimeout:  s.config.ReadTimeout,
        WriteTimeout: s.config.WriteTimeout,
        IdleTimeout:  s.config.IdleTimeout,
    }

    s.logger.Info("サーバー起動", map[string]interface{}{
        "address": s.server.Addr,
    })

    return s.server.ListenAndServe()
}

func (s *Server) Shutdown(ctx context.Context) error {
    s.logger.Info("サーバーシャットダウン開始")
    return s.server.Shutdown(ctx)
}

ルーティング設定

func (s *Server) setupRoutes() http.Handler {
    mux := http.NewServeMux()

    // ヘルスチェック
    mux.HandleFunc("/health", s.handleHealth)
    mux.HandleFunc("/ready", s.handleReady)

    // API エンドポイント
    mux.HandleFunc("/api/v1/users", s.handleUsers)
    mux.HandleFunc("/api/v1/users/", s.handleUser)
    mux.HandleFunc("/api/v1/register", s.handleRegister)
    mux.HandleFunc("/api/v1/login", s.handleLogin)

    // ミドルウェアを適用
    handler := s.applyMiddleware(mux)

    return handler
}

func (s *Server) applyMiddleware(handler http.Handler) http.Handler {
    // 逆順に適用(最後のミドルウェアが最初に実行)
    handler = RecoveryMiddleware(s.logger)(handler)
    handler = RequestIDMiddleware()(handler)
    handler = LoggingMiddleware(s.logger)(handler)
    handler = CORSMiddleware()(handler)
    return handler
}

ハンドラー実装

// internal/server/handlers.go
func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    // リクエストボディをパース
    var req struct {
        Email    string `json:"email"`
        Name     string `json:"name"`
        Password string `json:"password"`
    }

    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        s.respondError(w, http.StatusBadRequest, "無効なリクエスト")
        return
    }

    // サービス層を呼び出し
    user, err := s.service.Register(r.Context(), req.Email, req.Name, req.Password)
    if err != nil {
        s.logger.Error("ユーザー登録失敗", map[string]interface{}{
            "error": err.Error(),
        })
        s.respondError(w, http.StatusBadRequest, err.Error())
        return
    }

    // レスポンス
    s.respondJSON(w, http.StatusCreated, map[string]interface{}{
        "user": map[string]interface{}{
            "id":    user.ID,
            "email": user.Email,
            "name":  user.Name,
        },
    })
}

func (s *Server) respondJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func (s *Server) respondError(w http.ResponseWriter, status int, message string) {
    s.respondJSON(w, status, map[string]string{
        "error": message,
    })
}

ヘルスチェックエンドポイント

func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
    // シンプルなヘルスチェック
    s.respondJSON(w, http.StatusOK, map[string]string{
        "status": "ok",
    })
}

func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
    // 依存サービスの状態をチェック
    ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
    defer cancel()

    // データベース接続チェック
    if err := s.checkDatabase(ctx); err != nil {
        s.respondJSON(w, http.StatusServiceUnavailable, map[string]string{
            "status": "not ready",
            "reason": "database unavailable",
        })
        return
    }

    s.respondJSON(w, http.StatusOK, map[string]string{
        "status": "ready",
    })
}

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

シグナルハンドリング

// cmd/myapp/main.go
package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"

    "myapp/internal/config"
    "myapp/internal/logger"
    "myapp/internal/repository"
    "myapp/internal/server"
    "myapp/internal/service"
)

func main() {
    if err := run(); err != nil {
        fmt.Fprintf(os.Stderr, "エラー: %v\n", err)
        os.Exit(1)
    }
}

func run() error {
    // 設定読み込み
    cfg, err := config.Load()
    if err != nil {
        return fmt.Errorf("設定読み込み失敗: %w", err)
    }

    // ロガー初期化
    log := logger.New(os.Stdout, logger.InfoLevel, cfg.Logger.Format)

    // データベース接続
    db, err := InitDB(&cfg.Database)
    if err != nil {
        return fmt.Errorf("DB初期化失敗: %w", err)
    }
    defer db.Close()

    // リポジトリ、サービス、サーバー初期化
    repo := repository.NewUserRepository(db)
    svc := service.NewUserService(repo)
    srv := server.New(&cfg.Server, log, svc)

    // シグナルチャネル
    stop := make(chan os.Signal, 1)
    signal.Notify(stop, os.Interrupt, syscall.SIGTERM)

    // サーバー起動(別goroutine)
    serverErrors := make(chan error, 1)
    go func() {
        log.Info("サーバー起動中")
        serverErrors <- srv.Start()
    }()

    // シャットダウンまたはエラーを待機
    select {
    case err := <-serverErrors:
        return fmt.Errorf("サーバーエラー: %w", err)

    case sig := <-stop:
        log.Info("シャットダウンシグナル受信", map[string]interface{}{
            "signal": sig.String(),
        })

        // グレースフルシャットダウン
        ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
        defer cancel()

        if err := srv.Shutdown(ctx); err != nil {
            return fmt.Errorf("シャットダウン失敗: %w", err)
        }

        log.Info("サーバー正常終了")
    }

    return nil
}

シャットダウンシーケンス:

SIGTERM/SIGINT 受信
    │
    v
┌────────────────────────────────────┐
│ 1. 新規接続の受付停止               │
│    server.Shutdown() 呼び出し       │
└────────┬───────────────────────────┘
         │
         v
┌────────────────────────────────────┐
│ 2. アクティブリクエスト完了待機     │
│    - 最大30秒待機                   │
│    - 進行中の処理を完了させる       │
└────────┬───────────────────────────┘
         │
         v
┌────────────────────────────────────┐
│ 3. データベース接続クローズ         │
│    db.Close()                      │
└────────┬───────────────────────────┘
         │
         v
┌────────────────────────────────────┐
│ 4. プロセス終了                     │
│    os.Exit(0)                      │
└────────────────────────────────────┘

監視とメトリクス

Prometheusメトリクス

// internal/metrics/metrics.go
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 (
    // HTTPリクエスト総数
    httpRequestsTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "HTTPリクエストの総数",
        },
        []string{"method", "path", "status"},
    )

    // HTTPリクエスト期間
    httpRequestDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTPリクエストの処理時間",
            Buckets: prometheus.DefBuckets,
        },
        []string{"method", "path"},
    )

    // アクティブ接続数
    activeConnections = promauto.NewGauge(
        prometheus.GaugeOpts{
            Name: "active_connections",
            Help: "現在のアクティブ接続数",
        },
    )

    // データベースクエリ期間
    dbQueryDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "db_query_duration_seconds",
            Help:    "データベースクエリの実行時間",
            Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10},
        },
        []string{"query_type"},
    )
)

// メトリクスミドルウェア
func MetricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        activeConnections.Inc()
        defer activeConnections.Dec()

        rw := &responseWriter{
            ResponseWriter: w,
            statusCode:     http.StatusOK,
        }

        next.ServeHTTP(rw, r)

        duration := time.Since(start).Seconds()
        httpRequestsTotal.WithLabelValues(
            r.Method,
            r.URL.Path,
            fmt.Sprintf("%d", rw.statusCode),
        ).Inc()
        httpRequestDuration.WithLabelValues(
            r.Method,
            r.URL.Path,
        ).Observe(duration)
    })
}

// メトリクスエンドポイント
func Handler() http.Handler {
    return promhttp.Handler()
}

アプリケーションメトリクス

// ビジネスメトリクス
var (
    userRegistrations = promauto.NewCounter(
        prometheus.CounterOpts{
            Name: "user_registrations_total",
            Help: "ユーザー登録数",
        },
    )

    loginAttempts = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "login_attempts_total",
            Help: "ログイン試行数",
        },
        []string{"status"}, // success, failure
    )
)

// サービス層で使用
func (s *userService) Register(ctx context.Context, email, name, password string) (*models.User, error) {
    user, err := s.createUser(ctx, email, name, password)
    if err != nil {
        return nil, err
    }

    userRegistrations.Inc()  // メトリクス記録
    return user, nil
}

テスト戦略

ユニットテスト

// internal/service/user_service_test.go
package service_test

import (
    "context"
    "testing"

    "myapp/internal/service"
    "myapp/internal/repository/mocks"
)

func TestUserService_Register(t *testing.T) {
    tests := []struct {
        name    string
        email   string
        pass    string
        wantErr bool
    }{
        {
            name:    "有効なユーザー",
            email:   "test@example.com",
            pass:    "password123",
            wantErr: false,
        },
        {
            name:    "短すぎるパスワード",
            email:   "test@example.com",
            pass:    "short",
            wantErr: true,
        },
        {
            name:    "無効なメール",
            email:   "invalid-email",
            pass:    "password123",
            wantErr: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // モックリポジトリ
            mockRepo := mocks.NewUserRepository()
            svc := service.NewUserService(mockRepo)

            _, err := svc.Register(context.Background(), tt.email, "Test User", tt.pass)
            if (err != nil) != tt.wantErr {
                t.Errorf("Register() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

統合テスト

// test/integration/api_test.go
package integration_test

import (
    "bytes"
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "myapp/internal/server"
)

func TestRegisterAPI(t *testing.T) {
    // テスト用サーバー起動
    srv := setupTestServer(t)

    reqBody := map[string]string{
        "email":    "test@example.com",
        "name":     "Test User",
        "password": "password123",
    }
    body, _ := json.Marshal(reqBody)

    req := httptest.NewRequest("POST", "/api/v1/register", bytes.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    w := httptest.NewRecorder()

    srv.ServeHTTP(w, req)

    if w.Code != http.StatusCreated {
        t.Errorf("期待するステータスコード: %d, 実際: %d", http.StatusCreated, w.Code)
    }

    var resp map[string]interface{}
    json.NewDecoder(w.Body).Decode(&resp)

    if _, ok := resp["user"]; !ok {
        t.Error("レスポンスにuserフィールドがありません")
    }
}

デプロイメント

Dockerfile

# ビルドステージ
FROM golang:1.21-alpine AS builder

WORKDIR /app

# 依存関係をキャッシュ
COPY go.mod go.sum ./
RUN go mod download

# ソースコードをコピーしてビルド
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o myapp ./cmd/myapp

# 実行ステージ
FROM alpine:latest

RUN apk --no-cache add ca-certificates

WORKDIR /root/

# ビルドステージから実行ファイルをコピー
COPY --from=builder /app/myapp .

EXPOSE 8080

CMD ["./myapp"]

Kubernetes マニフェスト

# deployments/k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: myapp:latest
        ports:
        - containerPort: 8080
        env:
        - name: SERVER_PORT
          value: "8080"
        - name: DB_HOST
          valueFrom:
            secretKeyRef:
              name: db-credentials
              key: host
        resources:
          requests:
            memory: "128Mi"
            cpu: "100m"
          limits:
            memory: "256Mi"
            cpu: "200m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: myapp-service
spec:
  selector:
    app: myapp
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: LoadBalancer

パフォーマンス最適化

プロファイリング

import _ "net/http/pprof"

func main() {
    // pprof エンドポイントを有効化
    go func() {
        log.Println(http.ListenAndServe("localhost:6060", nil))
    }()

    // アプリケーション起動
    run()
}

プロファイリングコマンド:

# CPUプロファイル
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

# メモリプロファイル
go tool pprof http://localhost:6060/debug/pprof/heap

# ゴルーチン
go tool pprof http://localhost:6060/debug/pprof/goroutine

ベンチマーク

// internal/service/user_service_bench_test.go
func BenchmarkUserService_Register(b *testing.B) {
    mockRepo := mocks.NewUserRepository()
    svc := service.NewUserService(mockRepo)
    ctx := context.Background()

    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        svc.Register(ctx, "test@example.com", "Test User", "password123")
    }
}

自己診断問題

以下の問題に答えて、理解度を確認しましょう:

  • アーキテクチャ: レイヤードアーキテクチャの各層(Presentation、Service、Repository)の責務を説明してください。
  • 設定管理: 環境変数から設定を読み込む利点は何ですか?ハードコーディングとの比較で説明してください。
  • ロギング: 構造化ログ(JSON形式)が、テキスト形式のログより優れている理由を3つ挙げてください。
  • データベース: コネクションプールのMaxOpenConnsMaxIdleConnsConnMaxLifetimeの適切な設定値とその理由を説明してください。
  • トランザクション: リポジトリパターンとトランザクション管理を組み合わせる利点は何ですか?
  • HTTPサーバー: ReadTimeoutWriteTimeoutIdleTimeoutの違いと、それぞれが保護する攻撃を説明してください。
  • グレースフルシャットダウン: server.Shutdown(ctx)が30秒でタイムアウトした場合、何が起こりますか?
  • メトリクス: Counter、Gauge、Histogramの違いと、それぞれをどのような場面で使用するか説明してください。
  • テスト: ユニットテストと統合テストの違いを、モックの使用という観点から説明してください。
  • Docker: マルチステージビルドを使う利点を、イメージサイズとセキュリティの観点から説明してください。
  • Kubernetes: Liveness ProbeとReadiness Probeの違いと、それぞれが失敗した場合の挙動を説明してください。
  • パフォーマンス: pprofを使ったCPUプロファイリングで、どのような問題を特定できますか?
  • まとめ

    本章では、本番環境で使える完全なGoアプリケーションの構築方法を学びました。

    🔑 重要ポイント

  • アーキテクチャ: レイヤー分離による保守性とテストの容易性
  • 設定管理: 環境変数による柔軟な設定システム
  • ログ: 構造化ログによる監視とデバッグの効率化
  • データベース: リポジトリパターンとコネクションプール管理
  • サービス層: ビジネスロジックの集約とトランザクション管理
  • HTTPサーバー: タイムアウト設定とミドルウェアチェーン
  • グレースフルシャットダウン: 安全なサービス停止
  • 監視: Prometheusメトリクスによる可観測性
  • テスト: ユニットテストと統合テストの組み合わせ
  • デプロイ: DockerとKubernetesによる本番環境への展開

💡 次のステップ: この章で学んだパターンを基に、自分のプロジェクトを構築してみましょう。実際のコードを書き、デプロイし、運用することで、真の理解が深まります。

⚠️ 本番環境への注意事項:

  • すべてのエンドポイントに適切な認証と認可を実装
  • レート制限でDoS攻撃を防御
  • 機密情報はシークレット管理システム(AWS Secrets Manager等)で管理
  • 定期的なセキュリティパッチとライブラリのアップデート
  • 本番環境でのデバッグログは無効化
  • バックアップと災害復旧計画の策定

これでGo言語の基礎から実践まで、すべての旅を完了しました。おめでとうございます!