← 戻る

Goでリソースリークを防ぐ1手法 #golang

2019-10-18

Go には GC があるのでメモリリークは基本発生しませんが、メモリ以外のリソースはリークします。 たとえばよくありそうなのがファイル閉じ忘れ。

[https://play.golang.org/p/h7cOUtlLkIk](https://play.golang.org/p/h7cOUtlLkIk)

```text 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](https://play.golang.org/p/jOUzrwHQe2j)
```text 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()呼び出しを警告することはできるかもだが、本末転倒になりそう リークさせたくないリソースがあって、多少パフォーマンスを犠牲にできる箇所では使えるかなと思いました。 以上です。