概要
Goでソース書いているとき、たまにループ内でdeferを使いたいときがあります。
何個もファイルを開いていくときとか。
そういうとき、ループ内で defer 書くと、GoLandさんが「ループ内で defer を直接使ってるよ!」って警告を出してくれるんですが、とか言って、処理を一つするたびにエラー分岐で f.Close() って呼ぶのは面倒だなーって思ってました。
Go使ってる人たちは、どうやってるんだろうって調べてみると
というのを発見。なるほど!ループの中で関数スコープをつくってやればいいんですねー。気づかなかったです。感謝。
サンプル
以下は、関数スコープを作る場合と作らない場合の違いを試してみました。
package defer_ import "fmt" // DeferInLoop は、deferをループ内で利用したい場合のやり方についてのサンプルです。 // // REFERNCES:: // - https://mattn.kaoriya.net/software/lang/go/20151212021608.htm // - https://stackoverflow.com/questions/45617758/defer-in-the-loop-what-will-be-better func DeferInLoop() error { // -------------------------------------------------------------- // 上のURLに書かれているように、deferは内部でLIFOキューで管理されている // ので、ループ内で defer をそのまま書くと、どんどんキューに溜まってしまう. // // なので、ループ内で defer する場合は、匿名関数を用意して // 関数スコープを作り、その中で defer するようにする。 // // 関数スコープが作られるので、ループが一回分終了するたびにスコープを // 抜け、その際に defer が実行される。 // // ループ内でファイルをオープンしていく処理などを書いている場合に特に注意。 // -------------------------------------------------------------- // NGパターン bad() fmt.Println("---------------------------------------") // OKパターン good() return nil } func bad() { // ループ内で defer をそのまま使っているので // LIFOキューにループ回数分の defer が溜まった後 // 関数を抜けるときに実行される。 // // なので、この場合は // defer: 09 // defer: 08 // ・ // ・ // defer: 00 // // という風に、逆順で出力される for i := 0; i < 10; i++ { //noinspection GoDeferInLoop defer fmt.Printf("defer: %02d\n", i) } } func good() { // ループ内で defer をそのまま使わずに関数スコープを作り // defer を使っている。そのため、ループ一回毎に defer が // 実行される。 // // なので、この場合は // defer: 00 // defer: 01 // ・ // ・ // defer: 09 // // という風に出力される for i := 0; i < 10; i++ { func(i int) { defer fmt.Printf("defer: %02d\n", i) }(i) } }
実行すると以下のようになります。
$ make run go run github.com/devlights/try-golang/cmd/trygolang -onetime -example "" ENTER EXAMPLE NAME: defer_in_loop [Name] "defer_in_loop" defer: 09 defer: 08 defer: 07 defer: 06 defer: 05 defer: 04 defer: 03 defer: 02 defer: 01 defer: 00 --------------------------------------- defer: 00 defer: 01 defer: 02 defer: 03 defer: 04 defer: 05 defer: 06 defer: 07 defer: 08 defer: 09
キレイに逆になりますね。
try-golang/defer_in_loop.go at master · devlights/try-golang · GitHub
サンプル2
以下は、ループ内でファイルをいっぱい開いた際の違いについて試してみました。
package defer_ import ( "fmt" "github.com/devlights/try-golang/lib/output" "github.com/devlights/try-golang/util/enumerable" "github.com/devlights/try-golang/util/mem" "io/ioutil" "os" "runtime" ) // DeferInLoopManyFiles は、deferをループ内で利用したい場合のやり方についてのサンプルです。 // ループ内で大量のファイルを開いて defer で close しようとしている場合の対処について。 // // REFERNCES:: // - https://mattn.kaoriya.net/software/lang/go/20151212021608.htm // - https://stackoverflow.com/questions/45617758/defer-in-the-loop-what-will-be-better func DeferInLoopManyFiles() error { // -------------------------------------------------------------- // 基本的な動作については、 defer_in_loop.go にて記載している。 // ここでは、ファイルハンドルをループ内でキャプチャさせたまま // defer で大量に登録した場合、メモリがどうなるかを検証する. // -------------------------------------------------------------- dir, err := initDirectory() if err != nil { return err } //noinspection GoUnhandledErrorResult defer os.RemoveAll(dir) output.Stdoutl("dir", dir) // ダメなパターン loopRange := enumerable.NewRange(1, 3000) memory := mem.NewMem(mem.Alloc(true), mem.TotalAlloc(false), mem.NumGC(true)) err = badDefer(dir, loopRange, memory) if err != nil { return err } runtime.GC() // 良いパターン _, _ = loopRange.Reset() err = goodDefer(dir, loopRange, memory) if err != nil { return err } memory.Print("main end") return nil } func initDirectory() (string, error) { dir, err := ioutil.TempDir("", "try-golang") if err != nil { return "", err } return dir, nil } func badDefer(dir string, r enumerable.Range, memory mem.Mem) error { // 現在のメモリ量を出力しておく memory.Print("badDefer - init") // 大量にファイル作って defer に登録. // (関数スコープを作らない版) for r.Next() { file, err := ioutil.TempFile(dir, fmt.Sprintf("try-golang-tmp-%02d", r.Current())) if err != nil { return err } // とりあえず適当にデータ入れておく err = ioutil.WriteFile(file.Name(), []byte("helloworld"), 0644) if err != nil { return err } // defer 登録 // GoLand 使っている場合はループ内でのdeferの利用を検知してくれる // サンプルなので抑止しておく //noinspection GoUnhandledErrorResult,GoDeferInLoop defer file.Close() } output.Stdoutl("[file count]", r.Current()+1) // 現在のメモリ量を出力しておく memory.Print("badDefer - before gc") runtime.GC() memory.Print("badDefer - after gc") return nil } func goodDefer(dir string, r enumerable.Range, memory mem.Mem) error { // 現在のメモリ量を出力しておく memory.Print("goodDefer - init") // 大量にファイル作って defer に登録. // (関数スコープを作る版) for r.Next() { err := func() error { file, err := ioutil.TempFile(dir, fmt.Sprintf("try-golang-tmp-%02d", r.Current())) if err != nil { return err } // とりあえず適当にデータ入れておく err = ioutil.WriteFile(file.Name(), []byte("helloworld"), 0644) if err != nil { return err } // defer 登録 // GoLand 使っている場合はループ内でのdeferの利用を検知してくれる // サンプルなので抑止しておく //noinspection GoUnhandledErrorResult,GoDeferInLoop defer file.Close() return nil }() if err != nil { return err } } output.Stdoutl("[file count]", r.Current()+1) // 現在のメモリ量を出力しておく memory.Print("goodDefer - before gc") runtime.GC() memory.Print("goodDefer - after gc") return nil }
try-golang/defer_in_loop_manyfiles.go at master · devlights/try-golang · GitHub
実行すると以下のようになります。
$ make run go run github.com/devlights/try-golang/cmd/trygolang -onetime -example "" ENTER EXAMPLE NAME: defer_in_loop_manyfiles [Name] "defer_in_loop_manyfiles" dir /tmp/try-golang012063143 badDefer - init ---------------------------- Alloc 332 KiB NumGC 0 [file count] 3000 badDefer - before gc ---------------------------- Alloc 2043 KiB NumGC 0 badDefer - after gc ---------------------------- Alloc 1017 KiB NumGC 1 goodDefer - init ---------------------------- Alloc 293 KiB NumGC 2 [file count] 3000 goodDefer - before gc ---------------------------- Alloc 1770 KiB NumGC 2 goodDefer - after gc ---------------------------- Alloc 293 KiB NumGC 3 main end ---------------------------- Alloc 293 KiB NumGC 3
違いは after gc
って出力しているところに表れていますね。ループ内で直接 defer 書いている方は gc しても減りきっていません。つまり、参照が保持されているということです。ループ内で関数スコープを作って defer している方はきっちり減っています。
過去の記事については、以下のページからご参照下さい。
- いろいろ備忘録日記まとめ
サンプルコードは、以下の場所で公開しています。
- いろいろ備忘録日記サンプルソース置き場