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アプリケーションを構築するための実践的な知識を学びました:
これらの知識は、実世界のGoアプリケーション開発で直接使用できます。Day 8では、最終プロジェクトとしてこれらを全て統合した完全なアプリケーションを構築します。