🐙

File.Close()しないと何が問題なのか?

2023/09/09に公開

ファイルの読み書きについて

多くのGoエンジニアが、ファイルの読み書き時に以下のように defer 処理でファイルのClose処理を行っていると思いますが、そもそも、

  • なぜClose処理をしないといけないのか?
  • Close処理しないと何が問題になるのか?

を調査・検証してみました。

読み込み

# 読み込み用ファイル用意
$ echo hello > tmpfile
$ cat tmpfile
hello
func main() {
	file, err := os.Open("tmpfile")
	if err != nil {
		log.Fatal(err)
	}
	defer file.Close()

	buf := make([]byte, 100)
	len, err := file.Read(buf)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(string(buf[:len]))
}

// 出力
// $ go run ./main.go
// hello

書き込み

func main() {
    file, err := os.Create("newfile")
	if err != nil {
		log.Fatal(err)
	}

	// p.s. 特に書き込み時はエラー処理を握りつぶさないよう無名関数でエラーハンドリングを行う
	defer func() {
		err := file.Close()
		if err != nil {
			log.Fatal(err)
		}
	}()

	file.WriteString("hoge fuga")
}

// 出力
// $ cat newfile 
// hoge fuga%   

結論

  • Close() はエラーハンドリング、および *File型の後始末をしているだけ
  • ファイルディスクリプタは、実際に Open()Create() を発行した時点で、 「発行したプロセスが終了したらファイルディスクリプタを閉じる」 ように設定済みである
  • リソースの観点でいうと、最悪 Close() を呼び出さなくてもメモリ圧迫にはならない(↑上記で述べたように、プロセス終了時にファイルディスクリプタを閉じるようになっているから)
  • セキュリティの観点で言えば、特に書き込み時においては、Close()し忘れた場合、意図しない書き込みや不正な書き込みが行われる可能性があるため、書き込み完了後は即座に Close()を呼び出すべき。

リソースの観点

「ファイルをOpen()したら必ずClose()するように。そうしないとメモリ圧迫になるから。」と慣習のようによく言われていると思いますが、本当にメモリ圧迫するのか?Close()しないとどうなるのか?と疑問に思ったので、実際にあえて Close()しない処理を行って、その挙動を確認してみました。

func main() {
	_, err := os.Open("tmpfile")
	if err != nil {
		log.Fatal(err)
	}
    // close処理を行わない
	// defer file.Close()

    time.Sleep(10 * time.Second)
}

上記のように、ファイルをOpen()した後にClose()を行わず、10秒間スリープさせる中で、ファイルディスクリプタはどうなるのか?プロセス終了後も残ったままになってしまうのか?を検証してみました。

# backgroundで起動
$ go run ./main.go &       
[1] 69999
# 現在のディレクトリ下でtmpfileを開いているファイルディスクリプタを表示
$ lsof +d ./ | grep tmpfile
main      70020 m.masafumi    3r   REG   1,14        6 79489415 ./tmpfile

~ 10秒後 ~
[1]  + done       go run ./main.go
$ lsof +d ./ | grep tmpfile
# 結果なし

結果は上記の通りとなり、Close()を呼び出さなくても、プロセス終了後ファイルディスクリプタは閉じられていることが分かります。

理由は、Open()を呼び出した時点で、内部的に「発行したプロセスが終了したらファイルディスクリプタを閉じる」ように設定されているからです。

この点に関しては分かりやすい資料があったので以下に引用します。

Goから学ぶI/O > Chapter2 ファイルの読み書き

(おまけ)ファイルクローズ
ここまで見てきたファイル操作の裏には、どれもシステムコールがありました。
なので「ファイルのClose()メソッドも、裏ではclose()のシステムコールを呼んでいるんでしょ?」と推測する方もいるかもしれません。
しかし実は、os.File型のClose()メソッドを掘り下げても、closeシステムコールに繋がるsyscall.Closeは出てきません。
これはなぜかというと、ファイルオープンの時点で「ファイルオープンしたプロセスが終了したら、自動的にファイルを閉じてください」というO_CLOEXECフラグを立てているからなのです。
そのため、Close()メソッドがやっているのは
エラー処理
対応するos.File型を使えなくする後始末
という側面が強いです。

セキュリティの観点

特にファイル書き込み時においては、Close()は重要です。
Close()処理を行わない場合、書き込み処理を際限なく行うことができてしまいます。

func main() {
	file, err := os.Create("newfile")
	if err != nil {
		log.Fatal(err)
	}

	file.WriteString("hoge\n")

	// close処理を行わない
	// defer func() {
	// 	err := file.Close()
	// 	if err != nil {
	// 		log.Fatal(err)
	// 	}
	// }()

	file.WriteString("この処理は書き込みたくない\n")
}

// 出力
// $ go run ./main.go && cat newfile
// hoge
// この処理は書き込みたくない

従って、書き込みたい内容を全て終えたら、明示的にClose()を行った方がより安全でしょう。

まとめ

Close()を行わないとなぜダメなのか?何が問題なのか?という部分を理解することができました。
「ファイルを開いたら閉じる」 というのは直感的にも理解しやすいと思うので、Close()処理は必ず行っていきたいと思います。

参考

Discussion