Day 7: 実践プロジェクト - 解説
統合的な実践演習の意義
Day 7は、これまでの6日間で学んだ全ての概念を統合し、実際のプロダクショングレードアプリケーションを構築する総合演習です。
---
アーキテクチャ設計の原則
レイヤー分離アーキテクチャ
┌─────────────────────────────────┐
│ HTTP Handler Layer │ ← リクエスト/レスポンス処理
├─────────────────────────────────┤
│ Business Logic Layer │ ← ドメインロジック
├─────────────────────────────────┤
│ Data Access Layer │ ← データ永続化
└─────────────────────────────────┘
このアーキテクチャにより:
- 関心の分離: 各レイヤーが明確な責任を持つ
- テスタビリティ: 各レイヤーを独立してテスト可能
- 保守性: 変更の影響範囲が限定される
- 拡張性: 新機能の追加が容易
---
コア概念の統合
1. 型システムとインターフェース(Day 1-3)
// Domain Model - 型安全性を最大限活用
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Completed bool `json:"completed"`
Priority int `json:"priority"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// リクエスト検証 - メソッドによるカプセル化
func (r *CreateTodoRequest) Validate() error {
if strings.TrimSpace(r.Title) == "" {
return errors.New("title is required")
}
if len(r.Title) > 200 {
return errors.New("title must be less than 200 characters")
}
if r.Priority < 0 || r.Priority > 5 {
return errors.New("priority must be between 0 and 5")
}
return nil
}
学んだこと:
- 構造体タグによるJSON変換の制御
omitemptyによる柔軟なシリアライゼーション- メソッドレシーバーによるドメインロジックのカプセル化
- エラー処理のベストプラクティス
2. 並行処理とスレッドセーフティ(Day 4-5)
type TodoStore struct {
mu sync.RWMutex // 読み取り/書き込みロック
todos map[int]*Todo
nextID int
}
// 読み取り専用操作 - RLockを使用
func (s *TodoStore) Get(id int) (*Todo, error) {
s.mu.RLock() // 複数の読み取りを許可
defer s.mu.RUnlock() // deferで確実にアンロック
todo, exists := s.todos[id]
if !exists {
return nil, errors.New("todo not found")
}
return todo, nil
}
// 書き込み操作 - Lockを使用
func (s *TodoStore) Create(req CreateTodoRequest) (*Todo, error) {
if err := req.Validate(); err != nil {
return nil, err
}
s.mu.Lock() // 排他的ロック
defer s.mu.Unlock() // deferで確実にアンロック
s.nextID++
todo := &Todo{
ID: s.nextID,
Title: strings.TrimSpace(req.Title),
Description: strings.TrimSpace(req.Description),
Completed: false,
Priority: req.Priority,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
s.todos[todo.ID] = todo
return todo, nil
}
重要なポイント:
RLock: 複数の読み取りゴルーチンが同時アクセス可能
- Lock: 1つの書き込みゴルーチンのみがアクセス可能
- パフォーマンス最適化に有効- deferによるロック解放:
- データ競合の防止:
3. HTTPハンドラーとルーティング
// http.Handlerインターフェースの実装
func (h *TodoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// CORS設定
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")
// プリフライトリクエスト処理
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
// パスベースルーティング
path := strings.TrimPrefix(r.URL.Path, "/api/todos")
switch {
case path == "" || path == "/":
h.handleTodos(w, r) // コレクション操作
case strings.HasPrefix(path, "/"):
h.handleTodoByID(w, r, strings.TrimPrefix(path, "/")) // リソース操作
default:
http.NotFound(w, r)
}
}
RESTful API設計の原則:
- リソース指向:
/api/todos - コレクション
- /api/todos/{id} - 単一リソース- HTTPメソッドの意味:
GET: 読み取り(冪等)
- POST: 作成(非冪等)
- PUT: 更新(冪等)
- DELETE: 削除(冪等)- 適切なステータスコード:
200 OK: 成功(GET, PUT)
- 201 Created: 作成成功(POST)
- 204 No Content: 成功、レスポンスボディなし(DELETE)
- 400 Bad Request: クライアントエラー
- 404 Not Found: リソース未存在
- 500 Internal Server Error: サーバーエラー4. ミドルウェアパターン
// ミドルウェアの型定義
type Middleware func(http.Handler) http.Handler
// ロギングミドルウェア
func loggingMiddleware(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),
)
})
}
// リカバリーミドルウェア
func recoveryMiddleware(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: %v", err)
respondError(w, http.StatusInternalServerError, "internal server error")
}
}()
next.ServeHTTP(w, r)
})
}
// ミドルウェアのチェーン
handler := recoveryMiddleware(loggingMiddleware(mux))
ミドルウェアの利点:
- 横断的関心事の分離: ロギング、認証、リカバリーなど
- 再利用性: 複数のハンドラーで共通のロジックを共有
- 合成可能: 複数のミドルウェアを組み合わせ可能
- 順序制御: ミドルウェアの適用順序を明示的に制御
5. Graceful Shutdown
// サーバー設定
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// 非同期でサーバー起動
go func() {
log.Printf("Server starting on %s", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed: %v", err)
}
}()
// シグナル待機
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
<-quit
log.Println("Shutting down server...")
// グレースフルシャットダウン
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
グレースフルシャットダウンの重要性:
---
テスト戦略
1. ユニットテスト
func TestTodoStore_Create(t *testing.T) {
store := NewTodoStore()
tests := []struct {
name string
req CreateTodoRequest
wantErr bool
}{
{
name: "valid todo",
req: CreateTodoRequest{
Title: "Test Todo",
Priority: 1,
},
wantErr: false,
},
{
name: "empty title",
req: CreateTodoRequest{
Title: "",
Priority: 1,
},
wantErr: true,
},
{
name: "invalid priority",
req: CreateTodoRequest{
Title: "Test",
Priority: 10,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
todo, err := store.Create(tt.req)
if (err != nil) != tt.wantErr {
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && todo.Title != tt.req.Title {
t.Errorf("Title = %v, want %v", todo.Title, tt.req.Title)
}
})
}
}
テーブル駆動テスト:
- 複数のテストケースを構造的に管理
- コードの重複を削減
- テストケースの追加が容易
2. HTTPハンドラーテスト
func TestTodoHandler_CreateTodo(t *testing.T) {
store := NewTodoStore()
handler := NewTodoHandler(store)
reqBody := CreateTodoRequest{
Title: "Test Todo",
Description: "Test Description",
Priority: 2,
}
body, _ := json.Marshal(reqBody)
req := httptest.NewRequest(http.MethodPost, "/api/todos", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
// ステータスコード検証
if status := rr.Code; status != http.StatusCreated {
t.Errorf("handler returned wrong status code: got %v want %v",
status, http.StatusCreated)
}
// レスポンスボディ検証
var todo Todo
if err := json.NewDecoder(rr.Body).Decode(&todo); err != nil {
t.Errorf("failed to decode response: %v", err)
}
if todo.Title != reqBody.Title {
t.Errorf("Title = %v, want %v", todo.Title, reqBody.Title)
}
}
httptest パッケージ:
httptest.NewRequest: テスト用HTTPリクエスト作成httptest.NewRecorder: レスポンスをキャプチャ- 実際のHTTPサーバーを起動せずにテスト可能
3. 並行性テスト
func TestConcurrentAccess(t *testing.T) {
store := NewTodoStore()
const goroutines = 100
done := make(chan bool)
// 100個のゴルーチンで同時書き込み
for i := 0; i < goroutines; i++ {
go func(id int) {
store.Create(CreateTodoRequest{
Title: fmt.Sprintf("Todo %d", id),
Priority: 1,
})
done <- true
}(i)
}
// 全ゴルーチンの完了を待機
for i := 0; i < goroutines; i++ {
<-done
}
todos := store.List(nil)
if len(todos) != goroutines {
t.Errorf("expected %d todos, got %d", goroutines, len(todos))
}
}
並行性テストのポイント:
- データ競合の検出:
go test -race - 複数ゴルーチンの同期
- エッジケースの発見
4. ベンチマーク
func BenchmarkTodoStore_Create(b *testing.B) {
store := NewTodoStore()
req := CreateTodoRequest{
Title: "Benchmark Todo",
Priority: 1,
}
b.ResetTimer() // セットアップ時間を除外
for i := 0; i < b.N; i++ {
store.Create(req)
}
}
func BenchmarkConcurrentCreate(b *testing.B) {
store := NewTodoStore()
req := CreateTodoRequest{
Title: "Concurrent Todo",
Priority: 1,
}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
store.Create(req)
}
})
}
ベンチマーク実行:
go test -bench=. -benchmem
出力例:
BenchmarkTodoStore_Create-8 1000000 1234 ns/op 256 B/op 4 allocs/op
BenchmarkConcurrentCreate-8 5000000 456 ns/op 128 B/op 2 allocs/op
---
パフォーマンス最適化
1. メモリ割り当ての最小化
// 悪い例: 頻繁なメモリ割り当て
func List() []Todo {
result := []Todo{}
for _, todo := range s.todos {
result = append(result, *todo)
}
return result
}
// 良い例: 容量を事前確保
func List() []*Todo {
result := make([]*Todo, 0, len(s.todos))
for _, todo := range s.todos {
result = append(result, todo)
}
return result
}
2. ロックの粒度
// 悪い例: 粗粒度ロック
func (s *TodoStore) Update(id int, req UpdateTodoRequest) (*Todo, error) {
s.mu.Lock()
defer s.mu.Unlock()
// 長時間の処理(検証など)
if err := req.Validate(); err != nil {
return nil, err
}
todo := s.todos[id]
// 更新処理
return todo, nil
}
// 良い例: 細粒度ロック
func (s *TodoStore) Update(id int, req UpdateTodoRequest) (*Todo, error) {
// ロック外で検証
if err := req.Validate(); err != nil {
return nil, err
}
s.mu.Lock()
defer s.mu.Unlock()
todo := s.todos[id]
// 必要最小限の処理
return todo, nil
}
---
プロダクション化への道筋
1. データ永続化
// インターフェース定義
type TodoRepository interface {
Create(todo Todo) error
Get(id int) (*Todo, error)
List() ([]*Todo, error)
Update(todo Todo) error
Delete(id int) error
}
// メモリ実装
type InMemoryRepository struct {
mu sync.RWMutex
todos map[int]*Todo
}
// データベース実装
type PostgresRepository struct {
db *sql.DB
}
2. 認証・認可
// JWTミドルウェア
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
claims, err := validateJWT(token)
if err != nil {
respondError(w, http.StatusUnauthorized, "invalid token")
return
}
// コンテキストにユーザー情報を保存
ctx := context.WithValue(r.Context(), "user", claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
3. 構成管理
type Config struct {
Port string
DatabaseURL string
JWTSecret string
LogLevel string
}
func LoadConfig() (*Config, error) {
return &Config{
Port: getEnv("PORT", "8080"),
DatabaseURL: getEnv("DATABASE_URL", ""),
JWTSecret: getEnv("JWT_SECRET", ""),
LogLevel: getEnv("LOG_LEVEL", "info"),
}, nil
}
4. ロギング
import "github.com/rs/zerolog/log"
log.Info().
Str("method", r.Method).
Str("path", r.URL.Path).
Int("status", status).
Dur("duration", duration).
Msg("request completed")
---
次のステップ
Day 7の実践プロジェクトを通じて、Goでの実用的なWebアプリケーション開発の基礎を習得しました。
さらなる発展:
- フレームワークの学習: Gin, Echo, Chi
- データベース統合: sqlx, GORM
- gRPC: マイクロサービス通信
- Kubernetes: コンテナオーケストレーション
- observability: メトリクス、ログ、トレース
Day 8では、これらの概念を総復習し、Go Electivesへの準備を整えます。