ioutilやosでのファイル操作に対するリソース解放とエラーハンドリングの確認法

Goでは osioutil を用いたファイル操作が簡潔に書ける一方で、リソースリーク誤ったエラー処理が非常に起きやすい。

特に以下のような危険性が実務では頻発する。

  • defer を忘れてリソース解放漏れ
  • エラー処理が浅く、障害検知が遅れる
  • テスト容易性を無視した構造
  • 大規模ファイルを安易に ioutil.ReadFile() で読み込む
  • 権限・存在確認エラーの判定が雑

レビューアーは、単に「動いているか」ではなく、運用上問題になる設計構造が入り込んでいないかをチェックする必要がある。
本稿では実際のコード例を多用しながら、レビューアーが着目すべき構造的観点を整理していく。


1. 基本例:os.Open + defer Close の見落とし

まず最も初歩的だが重大なミスから確認する。

読み込み時の典型的なミス
func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    data, err := io.ReadAll(f)
    if err != nil {
        return nil, err
    }
    return data, nil
}

%% @Reviewer Close() が呼ばれていないため、ファイル記述子がリークするリスクがあります。defer f.Close() を適切な位置で宣言してください。

ファイル読み込み時は短時間で終わるため、レビューで見落とされがちだが、負荷試験・長期運用下ではFDリークが致命的になる。
リソース解放は「成功時」ではなく「Openできた時点で」即座に予約するのがGoの鉄則。


2. 正しい defer パターン:即時宣言の原則

正しいClose処理
func readFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    return io.ReadAll(f)
}

%% @Reviewer defer をオープン直後に書くことで、関数終了時に確実に Close() される構造になります。これはエラー発生時も含めた安全策です。

Goのレビューにおける「defer即宣言ルール」は、最優先の指摘事項である。


3. deferのスコープ外配置によるロジックバグ

deferの意味をなさない例
func readAndLog(path string) {
    var f *os.File
    var err error

    if f, err = os.Open(path); err != nil {
        log.Println("open failed:", err)
        return
    }

    defer f.Close() // この位置だと無意味
    // ...
}

%% @Reviewer defer の配置がエラー判定後になっており、Open失敗時に Close() が実行されません。err != nil の直後、成功確認後に即記述してください。

deferは「関数開始時に書くもの」ではなく、副作用が発生した直後に書くものと捉えるのがレビュー観点。


4. ioutil.ReadFile に逃げる設計の是非

ioutil使用例
func readSimple(path string) ([]byte, error) {
    return ioutil.ReadFile(path)
}

%% @Reviewer ReadFile() は便利だが、ファイルサイズ次第でOOMリスクとなります。大規模ファイルを扱う可能性があれば os.Open()+分割読み込みを検討してください。

ioutilについて

ioutil はGo 1.16で非推奨 (deprecated) となり、現在は os / io / os.ReadFile() への移行が推奨されています。

レビューアーは 「とりあえずReadFile」構造の氾濫 を早期に抑制する役割を持つ。


5. ファイル存在確認にありがちな誤判定

存在確認の雑な例
func exists(path string) bool {
    _, err := os.Stat(path)
    return err == nil
}

%% @Reviewer Stat エラーには アクセス権不足読み取りエラー も含まれます。存在判定は errors.Is(err, fs.ErrNotExist) で正確に行ってください。

改善例:

正確な存在確認
func exists(path string) bool {
    _, err := os.Stat(path)
    return !errors.Is(err, fs.ErrNotExist)
}

ファイル存在チェックは極めて多くのコードに現れ、レビューの重要指摘ポイントである。


6. ファイル書き込み時のエラー無視に注意

エラー無視例
func writeFile(path string, content []byte) {
    _ = os.WriteFile(path, content, 0644)
}

%% @Reviewer 書き込み失敗時に原因特定が不能となります。最低限のエラーログ出力、またはハンドリング層への伝播が必要です。

改善例:

エラーハンドリング付与
func writeFile(path string, content []byte) error {
    if err := os.WriteFile(path, content, 0644); err != nil {
        log.Printf("write failed: %v", err)
        return err
    }
    return nil
}

7. ファイル処理の抽象層を設けるレビュー視点

大規模サービスでは、直接 os を呼ばせず、抽象層で管理する設計が有効になる。
これにより以下が実現する:

  • テスト用モック実装が容易
  • 権限・監査対応の一元化
  • ロギング・監視の統合
抽象インターフェース例
type FileSystem interface {
    ReadFile(path string) ([]byte, error)
    WriteFile(path string, data []byte, perm fs.FileMode) error
}

type OSFileSystem struct{}

func (OSFileSystem) ReadFile(path string) ([]byte, error) {
    return os.ReadFile(path)
}

func (OSFileSystem) WriteFile(path string, data []byte, perm fs.FileMode) error {
    return os.WriteFile(path, data, perm)
}

%% @Reviewer FileSystem インターフェースにより副作用の切替容易性が確保されます。直接 os 利用を避ける設計はテスト可能性・移植性の観点で高評価です。


8. ファイル読み込み構造のエラーフロー全体像

UML Diagram

ファイル処理レビューはOpen失敗・Read失敗・Close漏れの全経路を脳内シミュレーションできる力が要求される。


9. 複雑ファイル処理でのリソース管理指摘例

中途半端な多段処理
func process(path string) error {
    f, err := os.Open(path)
    if err != nil {
        return err
    }
    defer f.Close()

    scanner := bufio.NewScanner(f)
    for scanner.Scan() {
        line := scanner.Text()
        if strings.HasPrefix(line, "WARN") {
            log.Println(line)
        }
    }
    return scanner.Err()
}

%% @Reviewer scanner.Err() はfor終了後に明示確認が必要です。読み込み中のエラーが隠蔽される構造になっています。

bufio.Scanner系のレビューはエラーチェックの明示忘れが最大の落とし穴となる。


10. defer Close() 内のエラーもレビュー対象

Close失敗の無視
defer f.Close()

実は Close() 自体が error を返すが、ほぼ無視されて書かれることが多い。
Goのレビュー文化では「Close()エラーは原則握りつぶして良い」が暗黙常識だが、重要なリソース破壊を伴う場合は確認すべきケースもある。

厳格Closeエラーハンドリング
defer func() {
    if cerr := f.Close(); cerr != nil {
        log.Printf("close error: %v", cerr)
    }
}()

%% @Reviewer 特殊ファイル・デバイスファイル・ネットワークFD等を扱うケースでは Close エラーも記録対象としてください。


11. パフォーマンス観点でのレビュー指摘例

非効率的な逐次Read
func read(path string) ([]byte, error) {
    f, _ := os.Open(path)
    defer f.Close()

    var buf []byte
    tmp := make([]byte, 1024)
    for {
        n, err := f.Read(tmp)
        if n > 0 {
            buf = append(buf, tmp[:n]...)
        }
        if err == io.EOF {
            break
        }
        if err != nil {
            return nil, err
        }
    }
    return buf, nil
}

%% @Reviewer 毎回スライスappendしておりメモリコピーが多発しています。 bytes.Buffer の活用により効率化が可能です。

改善案
func read(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()

    var buf bytes.Buffer
    if _, err := io.Copy(&buf, f); err != nil {
        return nil, err
    }
    return buf.Bytes(), nil
}

12. ファイル系レビュー観点まとめチェックリスト

観点 内容
defer f.Close() の即宣言 Open直後に記述
defer のスコープ外配置 if文ブロックの外などに置いていないか
ioutil.ReadFile() 乱用抑制 大容量処理では使用回避
エラーパスの検証網羅性 Open, Read, Close 各失敗経路を確認
scanner.Err() 確認有無 for文後の明示確認
Close() のエラーハンドリング 必要ならログ残し
errors.Is() による判定精度 fs.ErrNotExist 検知正確性
テスト容易性 抽象 FileSystem 導入の有無

13. まとめ:ファイル操作レビューは「静かなる設計崩壊」を防ぐ

  • リソースリークは静かに蓄積し、ある日突然クラッシュとして顕在化する
  • エラー無視構造は障害解析を困難にする
  • テスト不能構造は保守開発の首を絞める
  • OS依存コードはインターフェース抽象で隠蔽すべき

レビューアーがコードの内側で発生し得る副作用・隠れ経路・想定外パスを読み解くことこそが、実務現場での本当のレビュースキルである。

UML Diagram

以上が、実務レベルでのファイル系レビューにおける構造的ガイドである。