Goのdeferに潜むパフォーマンスの落とし穴とレビュー観点

Go言語の defer 構文は、関数の終了時に任意の処理を遅延実行するための構文です。
Close()Unlock() のように、忘れると致命的な操作を確実に実行するという目的において非常に有効です。

一方で、構文的に書けるからといって使ってしまうと、リソースの過保持やスコープ誤認、パフォーマンスの劣化を招くことがあります。
本記事では、レビューアーとしてどのように defer の使用意図・構造・実行コストを評価すべきか、具体例と共に解説します。


良い実装:deferによるスコープ完結の後始末

func writeLog(path string, message string) error {
    f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    if err != nil {
        return err
    }
    defer f.Close()

    _, err = fmt.Fprintln(f, message)
    return err
}

このコードでは、ファイルがオープンされたあと、関数終了時に必ず f.Close() が呼ばれます
複数の return パスがあっても漏れがなく、安全な構造となっています。

スコープの入り口でリソースを獲得し、出口で確実に解放する構造が一致している場合、defer は非常に有効です。


deferの技術的特性と落とし穴

評価タイミングは「即時」実行は「遅延」

defer評価タイミング
func example(path string) {
    defer fmt.Println("closing:", path)
    path = "changed.txt"
}

このコードでは "closing: changed.txt" ではなく "closing: original.txt" と出力されます。
これは defer に渡された path が、defer文が実行された時点で評価されるためです。

defer文が記述された位置で変数の値がキャプチャされる点に注意。後続で値が変更されても反映されません。


forループ内でのdeferはリソース保持の罠

func processFiles(paths []string) {
    for _, path := range paths {
        f, _ := os.Open(path)
        defer f.Close()
    }
}

上記では、すべての f.Close()processFiles の終了まで遅延されるため、一時的に大量のファイルディスクリプタが保持される構造となります。

deferは関数終了時まで実行されません。ループ内で使用することで、保持期間が意図より長くなりリソースを圧迫する危険があります。

改善案(即時close)

func processFiles(paths []string) {
    for _, path := range paths {
        f, _ := os.Open(path)
        process(f)
        f.Close()
    }
}

改善案(関数分離)

func processFiles(paths []string) {
    for _, path := range paths {
        handleOne(path)
    }
}

func handleOne(path string) {
    f, _ := os.Open(path)
    defer f.Close()
    process(f)
}

ロック解除のdeferと粒度の一貫性

func update(mu *sync.Mutex) {
    mu.Lock()
    defer mu.Unlock()
    doSomething()
}

この構造では doSomething() のみをロック対象にする意図かもしれませんが、将来 doSomething() の後に処理が追加された際、意図しないロック保持状態になる可能性があります。

deferでロック解除を行う場合、そのdeferがカバーする処理の粒度が意図と一致しているか?
将来のコード追加でロックスコープが曖昧化しないか?をレビュー時に確認する必要があります。


パフォーマンスへの影響

比較ベンチマーク
func withDefer() {
    defer func() {}()
}

func noDefer() {
    func() {}()
}
呼び出し方式 平均実行時間(ns/op)
withDefer 約 20.1
noDefer 約 1.2

Go 1.14以降で defer のオーバーヘッドは削減されていますが、ゼロにはなりません。
高頻度で呼び出されるループ内や性能クリティカルな経路での使用は慎重に検討する必要があります。


❌ deferの誤用とレビュー指摘

以下は典型的な誤用です。

func handle(reqs []Request, mu *sync.Mutex) {
    for _, r := range reqs {
        mu.Lock()
        defer mu.Unlock()
@Reviewer
この構造では、ループの回数分 `mu.Lock()` され、`Unlock()` がすべて後から実行されるため、競合やデッドロックの原因になります。ロック制御は関数に切り出すか、明示的に解除しましょう。
process(r) } }

このような構造では、mu.Lock() はループのたびに呼び出される一方で、defer mu.Unlock() はループ終了後にまとめて実行されてしまうため、デッドロックやパニックを招く危険があります。

改善例
func handle(reqs []Request, mu *sync.Mutex) {
    for _, r := range reqs {
        handleOne(r, mu)
    }
}

func handleOne(r Request, mu *sync.Mutex) {
    mu.Lock()
    defer mu.Unlock()
    process(r)
}

このように defer のスコープが関数全体と一致するよう切り出すことで、安全なロック制御が可能になります。


レビュー観点チェックリスト

観点カテゴリ チェック内容
評価タイミング defer文が記述された時点で評価される変数値が意図通りか?
リソース保持 ファイルやロックなど、保持期間がスコープと合っているか?
パフォーマンス tight loop 内や頻出パスに defer が含まれていないか?
スコープ設計 deferの範囲がロジック上の責務単位と一致しているか?
可読性と意図明示 明示的に Close()Unlock() を記述した方が伝わるケースではないか?

あとがき:構文の便利さに設計を委ねすぎない

deferはGoらしい簡潔さと安全性を備えた構文ですが、「便利だから使う」ではなく、「設計の意図を明確に表現できるか」で使うかを判断すべきです。

レビューアーとしては以下を意識してください:

  • 設計上の責務スコープと一致しているか?
  • 将来的な変更に対して堅牢な構造になっているか?
  • 意図が明確に読み取れるような位置に書かれているか?

deferのレビューは、「コードの動作」だけでなく「意図の可視性」「構造の妥当性」「コストとバランス」の視点からも評価することが求められます。