Web開発 - 解答例
この解答例では、本格的なタスク管理APIを実装します。以下の機能を含みます:
- RESTful APIの完全な実装
- データベース統合(PostgreSQL)
- ミドルウェア(ログ、認証、CORS、レート制限)
- バリデーションとエラーハンドリング
- 包括的なテストコード
プロジェクト構成
task-manager/
├── main.go
├── go.mod
├── go.sum
├── handlers/
│ ├── tasks.go
│ ├── tasks_test.go
│ └── auth.go
├── middleware/
│ ├── logger.go
│ ├── recovery.go
│ ├── cors.go
│ ├── auth.go
│ └── ratelimit.go
├── models/
│ ├── task.go
│ └── user.go
├── storage/
│ ├── memory.go
│ ├── postgres.go
│ └── storage.go
└── config/
└── config.go
1. 基本実装
main.go
package main
import (
"log"
"net/http"
"task-manager/handlers"
"task-manager/middleware"
"task-manager/storage"
)
func main() {
store := storage.NewMemoryStore()
taskHandler := handlers.NewTaskHandler(store)
mux := http.NewServeMux()
// Web UI
mux.HandleFunc("GET /", taskHandler.IndexPage)
mux.HandleFunc("POST /tasks/create", taskHandler.CreateTaskForm)
// REST API
mux.HandleFunc("GET /api/tasks", taskHandler.ListTasks)
mux.HandleFunc("GET /api/tasks/{id}", taskHandler.GetTask)
mux.HandleFunc("POST /api/tasks", taskHandler.CreateTask)
mux.HandleFunc("PUT /api/tasks/{id}", taskHandler.UpdateTask)
mux.HandleFunc("DELETE /api/tasks/{id}", taskHandler.DeleteTask)
handler := middleware.Logger(middleware.Recovery(mux))
addr := ":8080"
log.Printf("Server starting on %s", addr)
if err := http.ListenAndServe(addr, handler); err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}
models/task.go
package models
import "time"
type Task struct {
ID string `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Completed bool `json:"completed"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (t *Task) Validate() error {
if t.Title == "" {
return ErrEmptyTitle
}
if len(t.Title) > 100 {
return ErrTitleTooLong
}
return nil
}
var (
ErrEmptyTitle = &ValidationError{Field: "title", Message: "title cannot be empty"}
ErrTitleTooLong = &ValidationError{Field: "title", Message: "title must be 100 characters or less"}
)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return e.Message
}
storage/memory.go
package storage
import (
"fmt"
"sync"
"task-manager/models"
"time"
"github.com/google/uuid"
)
type MemoryStore struct {
mu sync.RWMutex
tasks map[string]*models.Task
}
func NewMemoryStore() *MemoryStore {
return &MemoryStore{
tasks: make(map[string]*models.Task),
}
}
func (s *MemoryStore) Create(task *models.Task) error {
s.mu.Lock()
defer s.mu.Unlock()
if err := task.Validate(); err != nil {
return err
}
task.ID = uuid.New().String()
task.CreatedAt = time.Now()
task.UpdatedAt = time.Now()
s.tasks[task.ID] = task
return nil
}
func (s *MemoryStore) Get(id string) (*models.Task, error) {
s.mu.RLock()
defer s.mu.RUnlock()
task, exists := s.tasks[id]
if !exists {
return nil, ErrTaskNotFound
}
return task, nil
}
func (s *MemoryStore) List() ([]*models.Task, error) {
s.mu.RLock()
defer s.mu.RUnlock()
tasks := make([]*models.Task, 0, len(s.tasks))
for _, task := range s.tasks {
tasks = append(tasks, task)
}
return tasks, nil
}
func (s *MemoryStore) Update(id string, task *models.Task) error {
s.mu.Lock()
defer s.mu.Unlock()
existing, exists := s.tasks[id]
if !exists {
return ErrTaskNotFound
}
if err := task.Validate(); err != nil {
return err
}
existing.Title = task.Title
existing.Description = task.Description
existing.Completed = task.Completed
existing.UpdatedAt = time.Now()
return nil
}
func (s *MemoryStore) Delete(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.tasks[id]; !exists {
return ErrTaskNotFound
}
delete(s.tasks, id)
return nil
}
var ErrTaskNotFound = fmt.Errorf("task not found")
handlers/tasks.go
package handlers
import (
"encoding/json"
"net/http"
"task-manager/models"
"task-manager/storage"
)
type TaskHandler struct {
store *storage.MemoryStore
}
func NewTaskHandler(store *storage.MemoryStore) *TaskHandler {
return &TaskHandler{store: store}
}
func (h *TaskHandler) ListTasks(w http.ResponseWriter, r *http.Request) {
tasks, err := h.store.List()
if err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
respondWithJSON(w, http.StatusOK, tasks)
}
func (h *TaskHandler) GetTask(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
respondWithError(w, http.StatusBadRequest, "task id is required")
return
}
task, err := h.store.Get(id)
if err != nil {
respondWithError(w, http.StatusNotFound, "task not found")
return
}
respondWithJSON(w, http.StatusOK, task)
}
func (h *TaskHandler) CreateTask(w http.ResponseWriter, r *http.Request) {
var task models.Task
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
respondWithError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.store.Create(&task); err != nil {
if _, ok := err.(*models.ValidationError); ok {
respondWithError(w, http.StatusBadRequest, err.Error())
return
}
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
respondWithJSON(w, http.StatusCreated, task)
}
func (h *TaskHandler) UpdateTask(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
respondWithError(w, http.StatusBadRequest, "task id is required")
return
}
var task models.Task
if err := json.NewDecoder(r.Body).Decode(&task); err != nil {
respondWithError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := h.store.Update(id, &task); err != nil {
if err == storage.ErrTaskNotFound {
respondWithError(w, http.StatusNotFound, "task not found")
return
}
if _, ok := err.(*models.ValidationError); ok {
respondWithError(w, http.StatusBadRequest, err.Error())
return
}
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
respondWithJSON(w, http.StatusOK, task)
}
func (h *TaskHandler) DeleteTask(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
respondWithError(w, http.StatusBadRequest, "task id is required")
return
}
if err := h.store.Delete(id); err != nil {
if err == storage.ErrTaskNotFound {
respondWithError(w, http.StatusNotFound, "task not found")
return
}
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
w.WriteHeader(http.StatusNoContent)
}
func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(payload)
}
func respondWithError(w http.ResponseWriter, code int, message string) {
respondWithJSON(w, code, map[string]string{"error": message})
}
middleware/logger.go
package middleware
import (
"log"
"net/http"
"time"
)
func Logger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
wrapper := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(wrapper, r)
log.Printf("%s %s %s %d %v",
r.RemoteAddr, r.Method, r.URL.Path,
wrapper.statusCode, time.Since(start))
})
}
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
middleware/recovery.go
package middleware
import (
"log"
"net/http"
"runtime/debug"
)
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\n%s", err, debug.Stack())
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
2. ミドルウェアの拡張
middleware/cors.go
package middleware
import "net/http"
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)
})
}
middleware/auth.go
package middleware
import (
"context"
"net/http"
"strings"
)
type contextKey string
const UserIDKey contextKey = "userID"
func Auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
// "Bearer <token>" 形式を期待
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid authorization header", http.StatusUnauthorized)
return
}
token := parts[1]
userID, err := validateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// ContextにユーザーIDを保存
ctx := context.WithValue(r.Context(), UserIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func validateToken(token string) (string, error) {
// 実際にはJWTの検証などを行う
// ここではシンプルな例として固定値を返す
if token == "valid-token" {
return "user123", nil
}
return "", fmt.Errorf("invalid token")
}
middleware/ratelimit.go
package middleware
import (
"net/http"
"sync"
"time"
"golang.org/x/time/rate"
)
type RateLimiter struct {
visitors map[string]*rate.Limiter
mu sync.RWMutex
r rate.Limit
b int
}
func NewRateLimiter(r rate.Limit, b int) *RateLimiter {
return &RateLimiter{
visitors: make(map[string]*rate.Limiter),
r: r,
b: b,
}
}
func (rl *RateLimiter) getVisitor(ip string) *rate.Limiter {
rl.mu.Lock()
defer rl.mu.Unlock()
limiter, exists := rl.visitors[ip]
if !exists {
limiter = rate.NewLimiter(rl.r, rl.b)
rl.visitors[ip] = limiter
// 古いエントリのクリーンアップ(別goroutineで)
go func() {
time.Sleep(3 * time.Minute)
rl.mu.Lock()
delete(rl.visitors, ip)
rl.mu.Unlock()
}()
}
return limiter
}
func (rl *RateLimiter) Limit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
limiter := rl.getVisitor(ip)
if !limiter.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
3. データベース統合
storage/storage.go
package storage
import "task-manager/models"
type Storage interface {
Create(task *models.Task) error
Get(id string) (*models.Task, error)
List() ([]*models.Task, error)
Update(id string, task *models.Task) error
Delete(id string) error
}
storage/postgres.go
package storage
import (
"context"
"database/sql"
"fmt"
"task-manager/models"
"time"
_ "github.com/lib/pq"
)
type PostgresStore struct {
db *sql.DB
}
func NewPostgresStore(connectionString string) (*PostgresStore, error) {
db, err := sql.Open("postgres", connectionString)
if err != nil {
return nil, err
}
// 接続プールの設定
db.SetMaxOpenConns(25)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
// 接続確認
if err := db.Ping(); err != nil {
return nil, err
}
return &PostgresStore{db: db}, nil
}
func (s *PostgresStore) Create(task *models.Task) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := task.Validate(); err != nil {
return err
}
query := `
INSERT INTO tasks (title, description, completed, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5)
RETURNING id
`
now := time.Now()
err := s.db.QueryRowContext(
ctx,
query,
task.Title,
task.Description,
task.Completed,
now,
now,
).Scan(&task.ID)
if err != nil {
return fmt.Errorf("failed to create task: %w", err)
}
task.CreatedAt = now
task.UpdatedAt = now
return nil
}
func (s *PostgresStore) Get(id string) (*models.Task, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `
SELECT id, title, description, completed, created_at, updated_at
FROM tasks
WHERE id = $1
`
task := &models.Task{}
err := s.db.QueryRowContext(ctx, query, id).Scan(
&task.ID,
&task.Title,
&task.Description,
&task.Completed,
&task.CreatedAt,
&task.UpdatedAt,
)
if err == sql.ErrNoRows {
return nil, ErrTaskNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to get task: %w", err)
}
return task, nil
}
func (s *PostgresStore) List() ([]*models.Task, error) {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `
SELECT id, title, description, completed, created_at, updated_at
FROM tasks
ORDER BY created_at DESC
`
rows, err := s.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to list tasks: %w", err)
}
defer rows.Close()
var tasks []*models.Task
for rows.Next() {
task := &models.Task{}
err := rows.Scan(
&task.ID,
&task.Title,
&task.Description,
&task.Completed,
&task.CreatedAt,
&task.UpdatedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan task: %w", err)
}
tasks = append(tasks, task)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("rows iteration error: %w", err)
}
return tasks, nil
}
func (s *PostgresStore) Update(id string, task *models.Task) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
if err := task.Validate(); err != nil {
return err
}
query := `
UPDATE tasks
SET title = $1, description = $2, completed = $3, updated_at = $4
WHERE id = $5
`
result, err := s.db.ExecContext(
ctx,
query,
task.Title,
task.Description,
task.Completed,
time.Now(),
id,
)
if err != nil {
return fmt.Errorf("failed to update task: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rows == 0 {
return ErrTaskNotFound
}
return nil
}
func (s *PostgresStore) Delete(id string) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
query := `DELETE FROM tasks WHERE id = $1`
result, err := s.db.ExecContext(ctx, query, id)
if err != nil {
return fmt.Errorf("failed to delete task: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rows == 0 {
return ErrTaskNotFound
}
return nil
}
func (s *PostgresStore) Close() error {
return s.db.Close()
}
マイグレーションスクリプト
-- migrations/001_create_tasks_table.sql
CREATE TABLE IF NOT EXISTS tasks (
id SERIAL PRIMARY KEY,
title VARCHAR(100) NOT NULL,
description TEXT,
completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);
CREATE INDEX idx_tasks_created_at ON tasks(created_at DESC);
CREATE INDEX idx_tasks_completed ON tasks(completed);
4. テストコード
handlers/tasks_test.go
package handlers
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"task-manager/models"
"task-manager/storage"
"testing"
)
func TestCreateTask(t *testing.T) {
store := storage.NewMemoryStore()
handler := NewTaskHandler(store)
task := models.Task{
Title: "Test Task",
Description: "Test Description",
}
body, _ := json.Marshal(task)
req := httptest.NewRequest("POST", "/api/tasks", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.CreateTask(rec, req)
if rec.Code != http.StatusCreated {
t.Errorf("Expected status %d, got %d", http.StatusCreated, rec.Code)
}
var result models.Task
json.NewDecoder(rec.Body).Decode(&result)
if result.Title != task.Title {
t.Errorf("Expected title %s, got %s", task.Title, result.Title)
}
if result.ID == "" {
t.Error("Expected task ID to be set")
}
}
func TestCreateTaskValidation(t *testing.T) {
store := storage.NewMemoryStore()
handler := NewTaskHandler(store)
tests := []struct {
name string
task models.Task
wantStatus int
}{
{
name: "empty title",
task: models.Task{Title: ""},
wantStatus: http.StatusBadRequest,
},
{
name: "title too long",
task: models.Task{Title: string(make([]byte, 101))},
wantStatus: http.StatusBadRequest,
},
{
name: "valid task",
task: models.Task{Title: "Valid Title"},
wantStatus: http.StatusCreated,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
body, _ := json.Marshal(tt.task)
req := httptest.NewRequest("POST", "/api/tasks", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.CreateTask(rec, req)
if rec.Code != tt.wantStatus {
t.Errorf("Expected status %d, got %d", tt.wantStatus, rec.Code)
}
})
}
}
func TestGetTask(t *testing.T) {
store := storage.NewMemoryStore()
handler := NewTaskHandler(store)
// タスクを作成
task := &models.Task{Title: "Test Task"}
store.Create(task)
req := httptest.NewRequest("GET", "/api/tasks/"+task.ID, nil)
req.SetPathValue("id", task.ID)
rec := httptest.NewRecorder()
handler.GetTask(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, rec.Code)
}
var result models.Task
json.NewDecoder(rec.Body).Decode(&result)
if result.ID != task.ID {
t.Errorf("Expected ID %s, got %s", task.ID, result.ID)
}
}
func TestListTasks(t *testing.T) {
store := storage.NewMemoryStore()
handler := NewTaskHandler(store)
// 複数のタスクを作成
for i := 0; i < 3; i++ {
task := &models.Task{Title: fmt.Sprintf("Task %d", i)}
store.Create(task)
}
req := httptest.NewRequest("GET", "/api/tasks", nil)
rec := httptest.NewRecorder()
handler.ListTasks(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, rec.Code)
}
var tasks []*models.Task
json.NewDecoder(rec.Body).Decode(&tasks)
if len(tasks) != 3 {
t.Errorf("Expected 3 tasks, got %d", len(tasks))
}
}
func TestUpdateTask(t *testing.T) {
store := storage.NewMemoryStore()
handler := NewTaskHandler(store)
// タスクを作成
task := &models.Task{Title: "Original Title"}
store.Create(task)
// 更新データ
updated := models.Task{
Title: "Updated Title",
Description: "Updated Description",
Completed: true,
}
body, _ := json.Marshal(updated)
req := httptest.NewRequest("PUT", "/api/tasks/"+task.ID, bytes.NewReader(body))
req.SetPathValue("id", task.ID)
rec := httptest.NewRecorder()
handler.UpdateTask(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("Expected status %d, got %d", http.StatusOK, rec.Code)
}
// 更新されたタスクを取得
result, _ := store.Get(task.ID)
if result.Title != updated.Title {
t.Errorf("Expected title %s, got %s", updated.Title, result.Title)
}
}
func TestDeleteTask(t *testing.T) {
store := storage.NewMemoryStore()
handler := NewTaskHandler(store)
// タスクを作成
task := &models.Task{Title: "To Delete"}
store.Create(task)
req := httptest.NewRequest("DELETE", "/api/tasks/"+task.ID, nil)
req.SetPathValue("id", task.ID)
rec := httptest.NewRecorder()
handler.DeleteTask(rec, req)
if rec.Code != http.StatusNoContent {
t.Errorf("Expected status %d, got %d", http.StatusNoContent, rec.Code)
}
// 削除されたことを確認
_, err := store.Get(task.ID)
if err != storage.ErrTaskNotFound {
t.Error("Expected task to be deleted")
}
}
ベンチマークテスト
func BenchmarkCreateTask(b *testing.B) {
store := storage.NewMemoryStore()
handler := NewTaskHandler(store)
task := models.Task{Title: "Benchmark Task"}
body, _ := json.Marshal(task)
b.ResetTimer()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("POST", "/api/tasks", bytes.NewReader(body))
rec := httptest.NewRecorder()
handler.CreateTask(rec, req)
}
}
func BenchmarkListTasks(b *testing.B) {
store := storage.NewMemoryStore()
handler := NewTaskHandler(store)
// 100個のタスクを作成
for i := 0; i < 100; i++ {
task := &models.Task{Title: fmt.Sprintf("Task %d", i)}
store.Create(task)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
req := httptest.NewRequest("GET", "/api/tasks", nil)
rec := httptest.NewRecorder()
handler.ListTasks(rec, req)
}
}
5. 設定管理
config/config.go
package config
import (
"os"
"strconv"
"time"
)
type Config struct {
ServerAddr string
DatabaseURL string
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
MaxOpenConnections int
MaxIdleConnections int
}
func Load() *Config {
return &Config{
ServerAddr: getEnv("SERVER_ADDR", ":8080"),
DatabaseURL: getEnv("DATABASE_URL", "postgres://localhost/taskdb?sslmode=disable"),
ReadTimeout: getDuration("READ_TIMEOUT", 10*time.Second),
WriteTimeout: getDuration("WRITE_TIMEOUT", 10*time.Second),
IdleTimeout: getDuration("IDLE_TIMEOUT", 60*time.Second),
MaxOpenConnections: getInt("MAX_OPEN_CONNS", 25),
MaxIdleConnections: getInt("MAX_IDLE_CONNS", 5),
}
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getDuration(key string, defaultValue time.Duration) time.Duration {
if value := os.Getenv(key); value != "" {
if d, err := time.ParseDuration(value); err == nil {
return d
}
}
return defaultValue
}
func getInt(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if i, err := strconv.Atoi(value); err == nil {
return i
}
}
return defaultValue
}
6. 完全なmain.go
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"task-manager/config"
"task-manager/handlers"
"task-manager/middleware"
"task-manager/storage"
"time"
"golang.org/x/time/rate"
)
func main() {
// 設定の読み込み
cfg := config.Load()
// データベース接続
store, err := storage.NewPostgresStore(cfg.DatabaseURL)
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
defer store.Close()
// ハンドラの初期化
taskHandler := handlers.NewTaskHandler(store)
// ルーティング設定
mux := http.NewServeMux()
// ヘルスチェック
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
// REST API
mux.HandleFunc("GET /api/tasks", taskHandler.ListTasks)
mux.HandleFunc("GET /api/tasks/{id}", taskHandler.GetTask)
mux.HandleFunc("POST /api/tasks", taskHandler.CreateTask)
mux.HandleFunc("PUT /api/tasks/{id}", taskHandler.UpdateTask)
mux.HandleFunc("DELETE /api/tasks/{id}", taskHandler.DeleteTask)
// ミドルウェアの適用
rateLimiter := middleware.NewRateLimiter(rate.Limit(10), 20)
handler := middleware.Logger(
middleware.Recovery(
middleware.CORS(
rateLimiter.Limit(mux),
),
),
)
// サーバー設定
srv := &http.Server{
Addr: cfg.ServerAddr,
Handler: handler,
ReadTimeout: cfg.ReadTimeout,
WriteTimeout: cfg.WriteTimeout,
IdleTimeout: cfg.IdleTimeout,
}
// Graceful shutdown
go func() {
log.Printf("Server starting on %s", cfg.ServerAddr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed: %v", err)
}
}()
// シグナルハンドリング
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
log.Println("Server shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
使用方法
1. 依存関係のインストール
go mod init task-manager
go get github.com/lib/pq
go get golang.org/x/time/rate
go get github.com/google/uuid
2. データベースのセットアップ
# PostgreSQLの起動(Dockerを使用)
docker run --name postgres \
-e POSTGRES_PASSWORD=password \
-e POSTGRES_DB=taskdb \
-p 5432:5432 \
-d postgres:15
# マイグレーションの実行
psql -h localhost -U postgres -d taskdb -f migrations/001_create_tasks_table.sql
3. サーバーの起動
export DATABASE_URL="postgres://postgres:password@localhost/taskdb?sslmode=disable"
go run main.go
4. APIのテスト
# タスクの作成
curl -X POST http://localhost:8080/api/tasks \
-H "Content-Type: application/json" \
-d '{"title":"My First Task","description":"This is a test"}'
# タスク一覧の取得
curl http://localhost:8080/api/tasks
# 特定のタスクの取得
curl http://localhost:8080/api/tasks/1
# タスクの更新
curl -X PUT http://localhost:8080/api/tasks/1 \
-H "Content-Type: application/json" \
-d '{"title":"Updated Task","completed":true}'
# タスクの削除
curl -X DELETE http://localhost:8080/api/tasks/1
5. テストの実行
# 全テストを実行
go test ./...
# カバレッジ付きで実行
go test -cover ./...
# ベンチマークを実行
go test -bench=. ./handlers
まとめ
この解答例では、以下を実装しました:
これらの実装パターンは、実際のプロダクション環境でも使用できる品質となっています。