deferって関数の最後で実行させたい処理に使うんでしょ、程度に考えていたが、きちんと理解をしようとdeferについて深堀りしてみた。
deferについてざっくり説明
- deferで処理を記述しておくとfuncの最後で実行してくれる。
- 以下の例だと、"今日は"が実行され、funcの終わりになるので、その後"金曜日です。"が実行される。
func hogehoge() {
defer fmt.Println("金曜日です。")
fmt.Println("今日は....")
}
今日は....
金曜日です。
ちょっと深堀り
Tour of Goの説明によると
- 囲まれているfunctionがreturnされるまで実行が見送られる。
- defer呼び出しの引数は即時に評価されるが、実行がreturnされるまで見送られる。
との説明がある。
1はなんとなく理解できるが、2の**"即時に評価される"**という記述が気になったのであとで詳しく解説したい。
また、プログラミング言語Goの説明によると
- deferを使うことでリソースの開放などがきれいに記述できる。
- deferの正しい位置は資源の獲得に成功した直後
という旨の説明がある。
以下が例である。
resp, err := http.Get(url)
defer resp.Body.Close() //本当はdeferの前にエラー判定いれるべき
Javaとかだと関数の最後にclose()を記述したりすることが多く、たまーにclose()を書き忘れたりすることもあるが、
goであればdefer使ってリソースを取得した直後に開放処理入れておくのが推奨されている。
即時に評価される、とは?
実際にコードを見たほうが早いと思うので、以下のサンプルを見て欲しい。
func main() {
defer today()()
fmt.Println("今日は....")
}
func today() func() {
fmt.Println("金曜日ですか?")
return func() {fmt.Println("それとも土曜日ですか?")}
}
これを実行するとどうなるだろうか?
deferはmainが終わる時に呼び出されるから、今日は...
がまず呼び出され、次にtoday()内の金曜日ですか?
、最後にそれとも土曜日ですか?
という流れるになるかと思うかもしれない。
しかし、実行してみると
金曜日ですか?
今日は....
それとも土曜日ですか?
となるのである。さてなぜだろう?
ポイントはtoday()の返り値が関数型である点だ。
前項の**defer呼び出しの引数は即時に評価されるが、**という部分に注目して欲しい。
defer today()()
の記述の部分で評価が実施されるのである。(なんで括弧が2つあるのか?と一瞬思う方もいるかもしれないが、話がそれるので最後に解説したい)
なので、
fmt.Println("金曜日ですか?")
が実行される。その後、main内の処理に進み、
fmt.Println("今日は....")
が実行される。
その後、遅延実行で
fmt.Println("それとも土曜日ですか?")
という流れである。
defer today()()の括弧が2つあるのはなぜ??
括弧2ついらないんじゃない??と思うひともいるかもしれないが、これは必要である。ためしに括弧を1つで実施するとどうなるだろうか?
func main() {
defer today()
fmt.Println("今日は....")
}
func today() func() {
fmt.Println("金曜日ですか?")
return func() {fmt.Println("それとも土曜日ですか?")}
}
今日は....
金曜日ですか?
順番が変わった上に、土曜日が消えている。。。。
defer today() //1
defer today()() //2
この2つの記述は根本的に違うのである。
1はあくまで、deferの処理でtoday関数を実行する。つまり、main関数の最後にtoday()が実行されるのである。この処理では無名関数がreturnされているが、無名関数は実行されないということなのである。
それに対し、2はtoday()でリターンされた関数をmain関数の最後に実施するのである。わかりやすく記述すると2はdeferの処理で無名関数func() {fmt.Println("それとも土曜日ですか?")}
を実行しているのである。