ioutilやosでのファイル操作に対するリソース解放とエラーハンドリングの確認法
ioutilやosでのファイル操作に対するリソース解放とエラーハンドリングの確認法
Goでは os
や ioutil
を用いたファイル操作が簡潔に書ける一方で、リソースリークや誤ったエラー処理が非常に起きやすい。
特に以下のような危険性が実務では頻発する。
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
パターン:即時宣言の原則
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のスコープ外配置によるロジックバグ
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
に逃げる設計の是非
func readSimple(path string) ([]byte, error) {
return ioutil.ReadFile(path)
}
%% @Reviewer ReadFile()
は便利だが、ファイルサイズ次第でOOMリスクとなります。大規模ファイルを扱う可能性があれば os.Open()
+分割読み込みを検討してください。
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. ファイル読み込み構造のエラーフロー全体像
ファイル処理レビューは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()
内のエラーもレビュー対象
defer f.Close()
実は Close()
自体が error
を返すが、ほぼ無視されて書かれることが多い。
Goのレビュー文化では「Close()エラーは原則握りつぶして良い」が暗黙常識だが、重要なリソース破壊を伴う場合は確認すべきケースもある。
defer func() {
if cerr := f.Close(); cerr != nil {
log.Printf("close error: %v", cerr)
}
}()
%% @Reviewer 特殊ファイル・デバイスファイル・ネットワークFD等を扱うケースでは Close
エラーも記録対象としてください。
11. パフォーマンス観点でのレビュー指摘例
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依存コードはインターフェース抽象で隠蔽すべき
レビューアーがコードの内側で発生し得る副作用・隠れ経路・想定外パスを読み解くことこそが、実務現場での本当のレビュースキルである。
以上が、実務レベルでのファイル系レビューにおける構造的ガイドである。