よくあるアンチパターン
よくあるアンチパターン
Section titled “よくあるアンチパターン”Goでよくあるアンチパターンと、実際に事故った構造を詳しく解説します。
A. リソースの「垂れ流し」
Section titled “A. リソースの「垂れ流し」”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: 例外を握りつぶしてファイルやDB接続を閉じないfunc processFile(filePath string) error { file, err := os.Open(filePath) if err != nil { return err } // 問題: defer文がないため、ファイルが閉じられない // 問題: エラー時にファイルが閉じられない // ファイル処理... return nil}なぜ事故るか:
- ファイル記述子の枯渇: ファイルが閉じられず、OSのファイル記述子が枯渇する
- 接続リーク: DB接続が閉じられず、接続プールが枯渇する
- 数時間後の停止: リソースリークは数時間後にシステム全体を停止させる
設計レビューでの指摘文例:
【指摘】リソースが適切に解放されていません。【問題】例外時にファイルやDB接続が閉じられず、リソースリークが発生します。【影響】ファイル記述子・接続プールの枯渇、数時間後のシステム停止【推奨】defer文で確実にリソースを解放するB. 無防備な待機
Section titled “B. 無防備な待機”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: 外部API呼び出しにタイムアウトを設定しないfunc createOrder(orderData OrderData) (*Order, error) { tx := db.Begin() defer func() { if r := recover(); r != nil { tx.Rollback() } }()
// 1. 注文を作成(データベースに保存) order := &Order{UserID: orderData.UserID, Amount: orderData.Amount} if err := tx.Create(order).Error; err != nil { tx.Rollback() return nil, err }
// 2. トランザクション内で外部APIを呼ぶ(問題) // 問題: タイムアウトが設定されていない // 問題: サーキットブレーカーがない resp, err := http.Post( "https://payment-api.example.com/charge", "application/json", bytes.NewBuffer(orderJSON), ) if err != nil { tx.Rollback() return nil, err }
// 3. 決済結果を保存 order.PaymentStatus = "COMPLETED" if err := tx.Save(order).Error; err != nil { tx.Rollback() return nil, err }
tx.Commit() return order, nil}なぜ事故るか:
- トランザクションの長時間保持: 外部APIの応答を待つ間、データベースのロックが保持される
- 外部障害の影響: 外部APIの障害がデータベーストランザクションに影響する
- ロールバックの困難: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
- タイムアウトのリスク: 外部APIの応答が遅い場合、トランザクションがタイムアウトする
- Goroutineプールの飽和: 遅延が連鎖してGoroutineプールが飽和し、全エンドポイントが応答不能に
実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: Goroutineリークfunc processOrders(orderIDs []int64) { for _, orderID := range orderIDs { // 問題: Goroutineが終了しない go func() { for { // 無限ループ(Goroutineが終了しない) processOrder(orderID) time.Sleep(1 * time.Second) } }() }}なぜ事故るか:
- Goroutineの蓄積: Goroutineが終了せず、メモリが枯渇する
- リソースの浪費: 終了しないGoroutineがリソースを占有する
- パフォーマンスの低下: Goroutineが増加すると、パフォーマンスが低下する
設計レビューでの指摘文例:
【指摘】Goroutineリークが発生しています。【問題】Goroutineが終了せず、メモリが枯渇します。【影響】メモリの枯渇、リソースの浪費、パフォーマンスの低下【推奨】context.Contextを使用してGoroutineを適切に終了させるC. 非冪等な再試行
Section titled “C. 非冪等な再試行”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: 再送時にデータが二重登録されるfunc createOrder(orderData OrderData) (*Order, error) { // 問題: Idempotency Keyがない // 問題: 再実行時に注文が二重作成される order := &Order{ UserID: orderData.UserID, Amount: orderData.Amount, } return orderRepo.Save(order)}
// クライアント側でリトライfunc createOrderWithRetry(orderData OrderData) error { for i := 0; i < 3; i++ { err := createOrder(orderData) if err == nil { return nil // 成功 } if i == 2 { return err // 最終リトライ失敗 } time.Sleep(time.Duration(i+1) * time.Second) // リトライ } return nil}なぜ事故るか:
- 二重登録: ネットワークエラーでクライアントが再送すると、注文が2つ作成される
- データの不整合: 同じ注文が複数存在し、在庫や決済に影響する
- ビジネスロジックの破綻: 重複データにより、ビジネスロジックが正しく動作しない
設計レビューでの指摘文例:
【指摘】非冪等な再試行が実装されています。【問題】再送時にデータが二重登録され、データの不整合が発生します。【影響】データの不整合、ビジネスロジックの破綻【推奨】Idempotency Keyを使用して冪等性を保証するD. Goroutineリーク
Section titled “D. Goroutineリーク”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: チャネルのデッドロックfunc processOrder(orderID int64) { ch := make(chan int)
go func() { // 問題: チャネルに送信するが、受信するgoroutineがない ch <- processOrderData(orderID) }()
// 問題: チャネルから受信するが、送信するgoroutineがブロックされる result := <-ch}なぜ事故るか:
- デッドロック: チャネルの送信と受信がブロックされる
- アプリケーションの停止: デッドロックにより、アプリケーションが停止する
設計レビューでの指摘文例:
【指摘】チャネルのデッドロックが発生しています。【問題】チャネルの送信と受信がブロックされます。【影響】アプリケーションの停止、デバッグの困難【推奨】バッファ付きチャネルを使用するか、送信と受信の順序を確認するE. チャネルのデッドロック
Section titled “E. チャネルのデッドロック”実際に事故った構造
Section titled “実際に事故った構造”// ❌ アンチパターン: チャネルのデッドロックfunc processOrder(orderID int64) { ch := make(chan int)
go func() { // 問題: チャネルに送信するが、受信するgoroutineがない ch <- processOrderData(orderID) }()
// 問題: チャネルから受信するが、送信するgoroutineがブロックされる result := <-ch}なぜ事故るか:
- デッドロック: チャネルの送信と受信がブロックされる
- アプリケーションの停止: デッドロックにより、アプリケーションが停止する
設計レビューでの指摘文例:
【指摘】チャネルのデッドロックが発生しています。【問題】チャネルの送信と受信がブロックされます。【影響】アプリケーションの停止、デバッグの困難【推奨】バッファ付きチャネルを使用するか、送信と受信の順序を確認するよくあるアンチパターンのポイント:
- A. リソースの「垂れ流し」: 例外を握りつぶしてファイルやDB接続を閉じない → 数時間後にシステム停止
- B. 無防備な待機: 外部API呼び出しにタイムアウトを設定しない → Goroutineプールの飽和、全エンドポイントの応答不能
- C. 非冪等な再試行: 再送時にデータが二重登録される → データの不整合、ビジネスロジックの破綻
- D. Goroutineリーク: Goroutineが終了せず、メモリが枯渇する
- E. チャネルのデッドロック: チャネルの送信と受信がブロックされる
これらのアンチパターンを避けることで、安全で信頼性の高いGoアプリケーションを構築できます。
重要な原則: 「正常に動く」よりも「異常時に安全に壊れる」ことを優先する。