Go には GC があるのでメモリリークは基本発生しませんが、メモリ以外のリソースはリークします。 たとえばよくありそうなのがファイル閉じ忘れ。
https://play.golang.org/p/h7cOUtlLkIk
package main
import (
"io/ioutil"
"log"
)
func main() {
f, err := ioutil.TempFile("", "tmp")
if err != nil {
log.Panic(err)
}
// f.Closeしてない!
f.Write([]byte("hoge"))
}
f.Close()するのを忘れてるのでリークしちゃってます。 リークを回避するために静的解析でがんばるというアプローチもあるかとは思うのですが、 今回は別の方法を考えてみました。
ioutil.TempFile()を直接使うのではなく、例えば次の例のようにラップしてリークしない安全なバージョンの関数を作ってみます。
https://play.golang.org/p/jOUzrwHQe2j
package main
import (
"io/ioutil"
"log"
"os"
)
func TempFileSafe(dir, pattern string, fn func(*os.File) error) error {
f, err := ioutil.TempFile("", "tmp")
if err != nil {
return err
}
defer f.Close()
return fn(f)
}
func main() {
err := TempFileSafe("", "tmp", func(f *os.File) error {
// TempFileSafe()の中で、テンポラリファイルを作ったあと
// 引数に渡した関数(つまりここの行)が呼ばれる
f.Write([]byte("hoge"))
return nil
})
if err != nil {
log.Panic(err)
}
// TempFileSafe()を抜けたあとはf.Close()が呼ばれてることを保証できるので、
// mainではf.Closeが不要
}
処理の流れとしては、以下のようになります。
- main()から TempFileSafe()を呼び出す
- TempFileSafe()は ioutil.TempFile()を呼び出す
- テンポラリファイル作成に成功したら、*io.File を引数に渡して TempFileSafe()第三引数の関数を呼び出す
- main にかかれている、TempFileSafe()の第三引数として渡した関数の中身が実行される
- TempFileSafe()に処理が戻って、defer f.Close()が実行されファイルが閉じられる
- TempFileSafe()の実行が完了したので main()に処理が戻る
- 終了
このアプローチについて、自分なりによい点、わるい点を考えてみました。
- Pros
- TempFileSafe()利用者はファイルを閉じることを考えなくてもよい
- ファイルだけでなく終了処理が必要な様々なリソースに同じアプローチを適用可能
- Cons
- ラップして Safe 版の関数を作るのが手間
- 関数呼び出しが余計に 1 個必要になるため、実行効率が比較的悪くなる
- ネストが深くなるため多用するとネスト地獄になりそう
- ioutil.TempFile()を使うこともできてしまう。静的解析で ioutil.TempFile()呼び出しを警告することはできるかもだが、本末転倒になりそう
リークさせたくないリソースがあって、多少パフォーマンスを犠牲にできる箇所では使えるかなと思いました。 以上です。