初めてGolangで書いたデータ投入ツールでプロセスがモリモリ肥大化していくのは
ループ内で defer hoge.Delete() とか書いてたせいだったらしい。
defer を消したら100〜200MB落ち着いている。
— m.yuzuki (@ephemeralsnow) December 11, 2015
golang の defer は後処理のキューの登録です。コードを見ていないので分かりませんが、おそらくこういうコードを書いたのだと推測します。
package main
import (
"fmt"
)
type foo struct {
n int
}
func Create(n int) *foo {
fmt.Printf("%v を作成\n", n)
return &foo{n}
}
func Delete(f *foo) {
fmt.Printf("%v を削除\n", f.n)
}
func main() {
fmt.Println("開始")
for i := 1; i <= 10; i++ {
f := Create(i)
defer Delete(f)
}
fmt.Println("終了")
}
この処理、実際には作成者の意図に反して以下の様に動作します。
開始
1 を作成
2 を作成
3 を作成
4 を作成
5 を作成
6 を作成
7 を作成
8 を作成
9 を作成
10 を作成
終了
10 を削除
9 を削除
8 を削除
7 を削除
6 を削除
5 を削除
4 を削除
3 を削除
2 を削除
1 を削除
つまり後処理を LIFO に登録し、関数スコープを抜けたタイミングで最後に登録したキューから取り出して実行します。ですので大量のループを実行するとキューが爆発します。さらに defer はその瞬間の変数をキャプチャします。
package main
import (
"os"
)
func doSomething() {
f, err := os.Open("test1.dat")
if err != nil {
return
}
defer f.Close() // test1.dat の Close()
f, err = os.Open("test2.dat")
if err != nil {
return
}
defer f.Close() // test2.dat の Close()
f, err = os.Open("test3.dat")
if err != nil {
return
}
defer f.Close() // test3.dat の Close()
}
func main() {
doSomething()
}
つまり test1.dat の f も test2.dat の f も test3.dat の f もキューに乗っかります。それを確認する為にこのコードを以下の様に修正します。
package main
import (
"fmt"
"os"
)
func closeFile(f *os.File) {
fmt.Printf("%v を Close() します\n", f.Name())
f.Close()
}
func doSomething() {
f, err := os.Open("test1.dat")
if err != nil {
return
}
defer closeFile(f) // test1.dat の Close()
f, err = os.Open("test2.dat")
if err != nil {
return
}
defer closeFile(f) // test2.dat の Close()
f, err = os.Open("test3.dat")
if err != nil {
return
}
defer closeFile(f) // test3.dat の Close()
}
func main() {
doSomething()
}
実行すると以下の様に出力されます(ファイルは存在しているものとします)。
test3.dat を Close() します
test2.dat を Close() します
test1.dat を Close() します
ですのでループの中で defer を使ってはいけません。ただしループの中で処理するステートメントが多く、defer を使って簡素化したい場合は、以下の様に関数スコープを作ってあげる必要があります。
package main
import (
"fmt"
)
type foo struct {
n int
}
func Create(n int) *foo {
fmt.Printf("%v を作成\n", n)
return &foo{n}
}
func Delete(f *foo) {
fmt.Printf("%v を削除\n", f.n)
}
func main() {
fmt.Println("開始")
for i := 1; i <= 10; i++ {
func() {
f := Create(i)
defer Delete(f)
// ... 色んな処理
}()
}
fmt.Println("終了")
}
もちろんこの場合、途中で return しても大域脱出にならないので注意が必要です。