第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つ挙げてください。
- データベース: コネクションプールの
MaxOpenConns、MaxIdleConns、ConnMaxLifetimeの適切な設定値とその理由を説明してください。 - トランザクション: リポジトリパターンとトランザクション管理を組み合わせる利点は何ですか?
- HTTPサーバー:
ReadTimeout、WriteTimeout、IdleTimeoutの違いと、それぞれが保護する攻撃を説明してください。 - グレースフルシャットダウン:
server.Shutdown(ctx)が30秒でタイムアウトした場合、何が起こりますか? - メトリクス: Counter、Gauge、Histogramの違いと、それぞれをどのような場面で使用するか説明してください。
- テスト: ユニットテストと統合テストの違いを、モックの使用という観点から説明してください。
- Docker: マルチステージビルドを使う利点を、イメージサイズとセキュリティの観点から説明してください。
- Kubernetes: Liveness ProbeとReadiness Probeの違いと、それぞれが失敗した場合の挙動を説明してください。
- パフォーマンス: pprofを使ったCPUプロファイリングで、どのような問題を特定できますか?
- アーキテクチャ: レイヤー分離による保守性とテストの容易性
- 設定管理: 環境変数による柔軟な設定システム
- ログ: 構造化ログによる監視とデバッグの効率化
- データベース: リポジトリパターンとコネクションプール管理
- サービス層: ビジネスロジックの集約とトランザクション管理
- HTTPサーバー: タイムアウト設定とミドルウェアチェーン
- グレースフルシャットダウン: 安全なサービス停止
- 監視: Prometheusメトリクスによる可観測性
- テスト: ユニットテストと統合テストの組み合わせ
- デプロイ: DockerとKubernetesによる本番環境への展開
まとめ
本章では、本番環境で使える完全なGoアプリケーションの構築方法を学びました。
🔑 重要ポイント:
💡 次のステップ: この章で学んだパターンを基に、自分のプロジェクトを構築してみましょう。実際のコードを書き、デプロイし、運用することで、真の理解が深まります。
⚠️ 本番環境への注意事項:
- すべてのエンドポイントに適切な認証と認可を実装
- レート制限でDoS攻撃を防御
- 機密情報はシークレット管理システム(AWS Secrets Manager等)で管理
- 定期的なセキュリティパッチとライブラリのアップデート
- 本番環境でのデバッグログは無効化
- バックアップと災害復旧計画の策定
これでGo言語の基礎から実践まで、すべての旅を完了しました。おめでとうございます!