Skip to content

よくあるアンチパターン

Goでよくあるアンチパターンと、実際に事故った構造を詳しく解説します。

// ❌ アンチパターン: 例外を握りつぶしてファイルやDB接続を閉じない
func processFile(filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
// 問題: defer文がないため、ファイルが閉じられない
// 問題: エラー時にファイルが閉じられない
// ファイル処理...
return nil
}

なぜ事故るか:

  1. ファイル記述子の枯渇: ファイルが閉じられず、OSのファイル記述子が枯渇する
  2. 接続リーク: DB接続が閉じられず、接続プールが枯渇する
  3. 数時間後の停止: リソースリークは数時間後にシステム全体を停止させる

設計レビューでの指摘文例:

【指摘】リソースが適切に解放されていません。
【問題】例外時にファイルやDB接続が閉じられず、リソースリークが発生します。
【影響】ファイル記述子・接続プールの枯渇、数時間後のシステム停止
【推奨】defer文で確実にリソースを解放する
// ❌ アンチパターン: 外部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
}

なぜ事故るか:

  1. トランザクションの長時間保持: 外部APIの応答を待つ間、データベースのロックが保持される
  2. 外部障害の影響: 外部APIの障害がデータベーストランザクションに影響する
  3. ロールバックの困難: 外部APIが成功した後にトランザクションが失敗した場合、外部APIのロールバックが困難
  4. タイムアウトのリスク: 外部APIの応答が遅い場合、トランザクションがタイムアウトする
  5. Goroutineプールの飽和: 遅延が連鎖してGoroutineプールが飽和し、全エンドポイントが応答不能に
// ❌ アンチパターン: Goroutineリーク
func processOrders(orderIDs []int64) {
for _, orderID := range orderIDs {
// 問題: Goroutineが終了しない
go func() {
for {
// 無限ループ(Goroutineが終了しない)
processOrder(orderID)
time.Sleep(1 * time.Second)
}
}()
}
}

なぜ事故るか:

  1. Goroutineの蓄積: Goroutineが終了せず、メモリが枯渇する
  2. リソースの浪費: 終了しないGoroutineがリソースを占有する
  3. パフォーマンスの低下: Goroutineが増加すると、パフォーマンスが低下する

設計レビューでの指摘文例:

【指摘】Goroutineリークが発生しています。
【問題】Goroutineが終了せず、メモリが枯渇します。
【影響】メモリの枯渇、リソースの浪費、パフォーマンスの低下
【推奨】context.Contextを使用してGoroutineを適切に終了させる
// ❌ アンチパターン: 再送時にデータが二重登録される
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
}

なぜ事故るか:

  1. 二重登録: ネットワークエラーでクライアントが再送すると、注文が2つ作成される
  2. データの不整合: 同じ注文が複数存在し、在庫や決済に影響する
  3. ビジネスロジックの破綻: 重複データにより、ビジネスロジックが正しく動作しない

設計レビューでの指摘文例:

【指摘】非冪等な再試行が実装されています。
【問題】再送時にデータが二重登録され、データの不整合が発生します。
【影響】データの不整合、ビジネスロジックの破綻
【推奨】Idempotency Keyを使用して冪等性を保証する
// ❌ アンチパターン: チャネルのデッドロック
func processOrder(orderID int64) {
ch := make(chan int)
go func() {
// 問題: チャネルに送信するが、受信するgoroutineがない
ch <- processOrderData(orderID)
}()
// 問題: チャネルから受信するが、送信するgoroutineがブロックされる
result := <-ch
}

なぜ事故るか:

  1. デッドロック: チャネルの送信と受信がブロックされる
  2. アプリケーションの停止: デッドロックにより、アプリケーションが停止する

設計レビューでの指摘文例:

【指摘】チャネルのデッドロックが発生しています。
【問題】チャネルの送信と受信がブロックされます。
【影響】アプリケーションの停止、デバッグの困難
【推奨】バッファ付きチャネルを使用するか、送信と受信の順序を確認する
// ❌ アンチパターン: チャネルのデッドロック
func processOrder(orderID int64) {
ch := make(chan int)
go func() {
// 問題: チャネルに送信するが、受信するgoroutineがない
ch <- processOrderData(orderID)
}()
// 問題: チャネルから受信するが、送信するgoroutineがブロックされる
result := <-ch
}

なぜ事故るか:

  1. デッドロック: チャネルの送信と受信がブロックされる
  2. アプリケーションの停止: デッドロックにより、アプリケーションが停止する

設計レビューでの指摘文例:

【指摘】チャネルのデッドロックが発生しています。
【問題】チャネルの送信と受信がブロックされます。
【影響】アプリケーションの停止、デバッグの困難
【推奨】バッファ付きチャネルを使用するか、送信と受信の順序を確認する

よくあるアンチパターンのポイント:

  • A. リソースの「垂れ流し」: 例外を握りつぶしてファイルやDB接続を閉じない → 数時間後にシステム停止
  • B. 無防備な待機: 外部API呼び出しにタイムアウトを設定しない → Goroutineプールの飽和、全エンドポイントの応答不能
  • C. 非冪等な再試行: 再送時にデータが二重登録される → データの不整合、ビジネスロジックの破綻
  • D. Goroutineリーク: Goroutineが終了せず、メモリが枯渇する
  • E. チャネルのデッドロック: チャネルの送信と受信がブロックされる

これらのアンチパターンを避けることで、安全で信頼性の高いGoアプリケーションを構築できます。

重要な原則: 「正常に動く」よりも「異常時に安全に壊れる」ことを優先する。