外部API呼び出し設計レビュー完全ガイド:タイムアウト・再試行・責務分離の実務判断

Goで外部APIを扱うコードは、レビュー上最も設計品質の差が出やすい領域である。
単に動くコードではなく、失敗を含めた安全設計ができているか、レビューアーは構造を読み解くことが要求される。

本稿は外部API呼び出し処理をレビューする際に実務で注視すべき観点を、良い設計例と詳細なレビュー指摘例を通じて解説するガイドである。


良い実装例:責務分離と制御可能性を両立した設計

type APIClient struct {
    client *http.Client
}

func NewAPIClient() *APIClient {
    return &APIClient{
        client: &http.Client{
            Timeout: 5 * time.Second,
        },
    }
}

func (c *APIClient) Fetch(ctx context.Context, url string) ([]byte, error) {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return nil, err
    }

    resp, err := c.client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    return io.ReadAll(resp.Body)
}
  • http.Clientの再利用によるコネクション管理の安定化
  • context.Contextによる呼び出し単位のタイムアウト制御
  • 責務分離によりテスト容易性・拡張性を確保

良くない実装例

1. http.Getによる非制御通信

func fetchData(url string) ([]byte, error) {
    resp, err := http.Get(url)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    return io.ReadAll(resp.Body)
}
@Reviewer
http.Get()はhttp.DefaultClientを使用し、Timeout未指定で無制御な長時間待機となります。専用のhttp.Clientを生成し、Timeoutを明示してください。

2. context未使用によるキャンセル不能設計

func callExternal(url string) ([]byte, error) {
    resp, err := http.Get(url)
    ...
}
@Reviewer
context未使用のため、上位設計でのキャンセル・トレース制御が不可能です。必ずcontextを受け取り、NewRequestWithContextで制御可能構造へ修正してください。

3. リトライ判定条件の欠如

func retry(f func() error, max int) error {
    for i := 0; i < max; i++ {
        err := f()
        if err == nil {
            return nil
        }
        time.Sleep(time.Duration(i+1) * 100 * time.Millisecond)
    }
    return errors.New("retry failed")
}
@Reviewer
失敗原因に関わらず盲目的に再試行しています。HTTPステータスコードやnet.Errorの一時性判定を導入し、再試行対象を限定してください。

4. タイムアウト粒度の混在

client := &http.Client{Timeout: 10 * time.Second}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
resp, err := client.Do(req)
@Reviewer
http.Client.Timeoutとcontext.Timeoutが競合しています。呼び出し単位の制御をcontext側に統一し、ClientはTransportレベルのみに使用してください。

5. 責務分離の欠落

func Handler(w http.ResponseWriter, r *http.Request) {
    // 直接API呼び出し実装
    ...
}
@Reviewer
ハンドラ内で外部API呼び出しロジックを直実装しています。APIクライアント層を分離し、責務分割を行ってください。

外部API呼び出し構造の責務分離モデル

UML Diagram

リトライ設計とタイムアウト制御フロー

UML Diagram

チェックリスト:外部API呼び出しレビュー観点

観点 チェック内容
http.Client再利用 都度生成されず、共通化・再利用されているか
Timeout設計 Timeout / context.WithTimeout により呼び出し単位で制御可能か
context受渡 全呼び出し経路でcontextが貫通しているか
再試行設計 一時的失敗のみを正しく再試行しているか
責務分離 APIクライアントが独立したモジュールに実装されているか
ロギング方針 失敗理由が追跡可能な出力が実装されているか
モック容易性 APIClientがinterface化・DI可能になっているか
上位停止制御 呼び出し側で処理全体のキャンセル制御が可能か

まとめ:外部APIレビューは「失敗前提設計力」を読む

API通信は「成功時の動作確認」では設計品質を見抜けない。レビューアーは以下の視点で失敗時も安全に崩れる構造を読み解く必要がある。

  • タイムアウト管理が設計レイヤで統制されているか
  • 再試行が安易に乱発されず妥当性判定されているか
  • contextにより呼び出し単位のキャンセルが可能か
  • 責務がハンドラ・ドメイン・通信層で整理されているか
  • テスト容易性(モック可能設計)が成立しているか

外部依存は常に落ちる可能性がある前提で設計されるべき責務である。レビューアーは「繋がった場合の設計」ではなく「繋がらなかった場合の耐性」を評価することが任務となる。