1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Go】「deferは関数終了時に実行される」その認識、大丈夫?

Last updated at Posted at 2024-09-29

はじめに

  • Goのdeferについて「呼び出し元の関数の終了時に実行されるんだな!」と軽く理解していませんか?(私もその一人でした、偉そうなタイトルですいません...)
  • ですが、そのような認識では説明できない動作に困惑する時が来るかもしれません
  • この記事では、私が実際に犯した勘違いを例に、deferの正しい動作を理解するためのポイントを解説します

前提:deferとは?

前回、deferの基本的な特徴をまとめた記事を書きました。一言で言うと、関数の遅延実行の仕組みです。詳しくはこちらをご覧ください。

私の勘違い

Goのdeferを初めて触れたとき、私はこう思っていました。

deferは関数の終了時に実行される。つまり、関数を抜けた後に一気に処理がされるんだな!!

しかし、この認識は完全に正確ではありません。特にdeferの引数が関数呼び出しを含む場合、この認識だと間違った結果になります。私がその間違いに気づいたのが次のソースコードとなります。

本題:出力結果を予測してみよう

では、私が引っかかったソースコードを見ていきましょう。出力順に①〜④の番号を振っています。これらの番号に対するmessageの出力値を予測してみてください。

sample1.go
package main

import (
	"fmt"
)

// グローバル変数
var message = "original message"

func changeMessage(newMessage string) (resetFunc func()) {
	// 現在のメッセージを保存
	tmp := message
	// 新しいメッセージに変更
	message = newMessage
	// 元に戻す関数を返す
	return func() {
		message = tmp
	}
}

func main() {
	// ①
	fmt.Println("Before doSomething:", message)

	// main関数の処理
	doSomething()

	// ④
	fmt.Println("After doSomething:", message)
}

func doSomething() {
	// ②
	fmt.Println("Before defer:", message)

	defer changeMessage("deferred message")()

	// ③
	fmt.Println("After defer:", message)
}

.
.
.
.
.

勘違いしていた時の私の予測

私の頭の中では、deferが最後に実行されるものだと信じていました。doSomethingの最後でmessagedeferred messageに変わるんでしょ?と。その為、次のような出力結果を予測していました。

.予測
Before doSomething: original message
Before defer: original message
After defer: original message
After doSomething: deferred message

しかし、実際の出力結果は次のようになります。

出力結果

sample1.go出力結果
Before doSomething: original message
Before defer: original message
After defer: deferred message
After doSomething: original message
  • 「え?After defermessagedeferred messageになっている??」
  • 「あれ、deferって呼び出し元の関数(今回で言うとdoSomething)の最後に実行されるんじゃないの??」

はい、当時の私はパニックです。

解説:deferは一番外側の関数を遅延実行させる仕組み

どうしてこのような出力結果となるのか、それは「deferの引数は宣言時に評価される」という特性を理解しきれていなかった為です。
deferに渡される引数が関数の場合、その引数の関数も即時評価(実行)されます。つまり、deferは一番外側の関数を遅延実行させる仕組みということとなります。

改めて、sample.1.goのdeferの記述を見てみましょう。

defer changeMessage("deferred message")()

この文では内側の関数をchangeMessage("deferred message")、外側の関数をchangeMessage戻り値の無名関数と分解できます。
その為、動作としては次のような流れになります。

  1. changeMessage("deferred message")が即時実行される
    • この時点でmessageの値はdeferred messageに変わります
  2. changeMessage戻り値の無名関数が、deferで遅延実行される関数として登録される
  3. doSomethingの終了時、deferに登録された無名関数が実行され、messageの値をoriginal messageに戻す

余談:括弧を省略した場合

余談になりますが、deferに渡す関数の実行部分、つまり末尾の括弧を除去すると、どうなるのでしょうか?試してみましょう。

// 省略

func doSomething() {
	// ②
	fmt.Println("Before defer:", message)

	// 末尾の括弧を除去してみる
    defer changeMessage("deferred message")

	// ③
	fmt.Println("After defer:", message)
}

この場合、changeMessage("deferred message")自身がdeferで遅延実行される関数として登録されます。

.実行結果
Before doSomething: original message
Before defer: original message
After defer: original message
After doSomething: deferred message

まとめ

今回のポイントです。

  • deferは一番外側の関数を遅延実行させる仕組みです
  • deferは「関数の終わりに実行される」ものではありますが、引数は即時評価されます
  • そして、deferに渡される引数が関数の場合、その引数の関数も即時評価(実行)されます

deferを使用する際は、引数の評価タイミングを理解しないと誤解を招くことがありますので注意しましょう!

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?