🚀

Go言語 deferの理解を確認する基本問題3選

2023/03/20に公開

概要

Go言語のdeferの挙動や仕様を確認する簡単なコードを紹介します。
初心者の方は勉強のために、中級者の方は理解の確認のために解いてみてください。

問題

それぞれ、コンソールに表示される文字列を考えてみてください。

問題1

func funcX(s string) {
	fmt.Print(s)
}
func funcY(s string) func() {
	fmt.Print(s)
	return func() { fmt.Print("Y") }
}
func main() {
	s := "A"
	defer funcX(s)
	s = "B"
	defer funcX(s)
	defer funcY(s)
	defer funcY(s)()
	defer func() {
		s = "C"
		fmt.Print(s)
	}()
}
正解を見る

問題2

func funcX() int {
	x := 0
	defer func() { x = 1 }()
	return x
}
func funcY() (y int) {
	y = 0
	defer func() { y = 1 }()
	return y
}
func main() {
	fmt.Print("x:", funcX())
	fmt.Print("y:", funcY())
}
正解を見る

問題3

func funcX() {
	defer func() {
		err := recover()
		fmt.Println("funcX recover:", err)
	}()
	log.Panic("panic")
}
func funcY() {
	defer func() {
		err := recover()
		fmt.Println("funcY recover:", err)
	}()
	log.Fatal("fatal")
}
func main() {
	funcX()
	funcY()
}
正解を見る

解説

番外編:無名関数について

そもそも無名関数の理解が曖昧だと、各問題の解説を読んでもピンとこないと思います。例えば、deferでよく見る下の形は無名関数を使っています。

defer func() {
	s = "C"
	fmt.Print(s)
}()

まずは、下記のコードを見てみましょう。
変数fに無名関数を格納し、f()で格納した無名関数を呼び出しています。

f := func() {
	s := "C"
	fmt.Println(s)
}
f()

変数を介さずに無名関数を即時呼び出ししたい場合は下記の様になります。

func() {
	s := "C"
	fmt.Println(s)
}()

後はdeferに渡しているかどうかの違いだけですね。

問題1

func funcX(s string) {
	fmt.Print(s)
}
func funcY(s string) func() {
	fmt.Print(s)
	return func() { fmt.Print("Y") }
}
func main() {
	s := "A"
	defer funcX(s)
	s = "B"
	defer funcX(s)
	defer funcY(s)
	defer funcY(s)()
	defer func() {
		s = "C"
		fmt.Print(s)
	}()
}

下記の理解を確認する問題です。

  1. deferの実行順序
  2. deferに渡した関数の引数の評価タイミング

1.defer の実行順序

deferに渡した処理はreturnされた後や関数の末尾に到達した後に実行されます。そして、deferは LIFO(スタック)のデータ構造になっています。下図の紫の線が通常のプログラムの実行順序で、青の線がdeferの実行順序になります。

よって、funcY(s)func()funcY(s)()funcY(s)funcX(s)funcX(s) の順に実行されます。

  1. funcY(s)
    • Bを表示して、無名関数を返します
  2. func()
    • Cを表示します
  3. funcY(s)()
    • funcY(s)の戻り値である無名関数を実行し、Yを表示します
  4. funcY(s)
    • Bを表示して、無名関数を返します
    • ここで返された無名関数は実行されていません
  5. funcX(s)
    • Bを表示します
  6. funcX(s)
    • Aを表示します
    • ここがAになる理由について、続けて解説していきます。

2.defer に渡した関数の引数は即時評価される

func funcX(s string) {
	fmt.Print(s)
}
func main() {
	s := "A"
	defer funcX(s)
	s = "B"
	defer funcX(s)
}

関数が最後まで実行された段階では、変数sにはBという値が入っています。その場合、1つ目のfuncX(s)の処理でもBが表示されそうですが、実際はAが表示されます。

これは 「defer に渡した関数の引数は即時評価される」 という仕様があるからです。

deferに渡した処理がreturnされた後や関数の末尾に到達した後に実行されるということに囚われると混乱しますが、逆にこの仕様が無かった場合を想像してみましょう。

この仕様がなければ、変数を追ってコードを読み解くのが難しくなったり、意図しない挙動が起こったりすることが予測できると思います(そもそも変数への再代入は避けるべきですが…)。

問題2

func funcX() int {
	x := 0
	defer func() { x = 1 }()
	return x
}
func funcY() (y int) {
	y = 0
	defer func() { y = 1 }()
	return y
}
func main() {
	fmt.Println("x:", funcX())
	fmt.Println("y:", funcY())
}

deferに渡した関数が、外側の関数の戻り値にアクセスするためには、名前付き戻り値を使う必要があります。よって、funcX()は戻り値にアクセスできず、値は0のまま、funcY()はアクセスでき、値は1となります。

この仕組みはエラー制御で多用されています。

func sendRequest(req Request) (err error) {
	conn, err := openConnection()
	if err != nil {
		return err
	}
	defer func() {
		err = multierr.Append(err, conn.Close())
	}()
	// ...
}

上記は、ライブラリ uber-go/multierr のサンプルコードです。関数内で発生したエラー情報を握り潰さずにdefer内で発生したエラー情報を加えた上で呼び出し元に返すよう実装されています。

問題3

func funcX() {
	defer func() {
		err := recover()
		fmt.Println("funcX recover:", err)
	}()
	log.Panic("panic")
}
func funcY() {
	defer func() {
		err := recover()
		fmt.Println("funcY recover:", err)
	}()
	log.Fatal("fatal")
}
func main() {
	funcX()
	funcY()
}

panicの方のみが表示され、fatalは表示されません。これは、os.Exitを使ってプログラムを強制終了するとdeferが実行されないからです。実はlog.Fatal系はos.Exit(1)を呼び出しています。
https://github.com/golang/go/blob/master/src/log/log.go#L260-L276

よって、コードレビューでlog.Fatal()を見たら注視して、誤っていたら指摘しましょう。log.Fatal()が許されるのはmain()init()、初期化処理くらいだと思います。

Go言語 おすすめ書籍

最後に、おすすめの関連書籍を紹介して終わりにします。
Go言語関連は色々読みましたが、この2冊が圧倒的にオススメです。

https://www.oreilly.co.jp/books/9784814400041/

とにかく詳細で、情報量が多いです。訳者の方が作成した付録も良いです。
他の言語を経験済みで、Goの勉強を始めようという方に特におすすめです。

https://www.oreilly.co.jp/books/9784873119694/

名前の通り、実用的で現場寄りの内容です。訳書ではないので、O'Reilly 特有の読みにくさもありません。Goをある程度使える人がステップアップのために読むとよいでしょう。

以上、この記事で何か一つでも学びがあれば幸いです。

GitHubで編集を提案

Discussion