2015/12/12

Recent entries from same category

  1. Go 言語プログラミングエッセンスという本を書きました。
  2. errors.Join が入った。
  3. unsafe.StringData、unsafe.String、unsafe.SliceData が入った。
  4. Re: Go言語で画像ファイルか確認してみる
  5. net/url に JoinPath が入った。

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 しても大域脱出にならないので注意が必要です。

Posted at by