blog

ゴー・ディファーの使い方と落とし穴

この記事では、deferについての理解を、ある問題から別の問題へとリフレッシュしたいと思います。deferは、Go言語が提供するメカニズムで、遅延された呼び出しを登録することで、一部のリソースを確実に...

Feb 18, 2020 · 8 min. read
シェア

序文

より良いユヅ体験のために、まずはマニュアルカタログを

囲碁を始めたばかりの学生は、愛されもすれば嫌われもするdeferについて知っておくべきです。クロージャーと組み合わせると、さらに殺人的な効果があり、使い方を知っている人は万人の敵を傷つけ、使い方を知らない人は自分自身を傷つけているのです。

この記事では、deferを再認識していただくために、1つの質問から別の質問までご紹介したいと思います。

全文を紹介するために簡単な質問を投げかけましょう。次のプログラムの正しい出力は何ですか?

package main
import "fmt"
func main() {
	fmt.Println(f1())
	fmt.Println(f2())
}
func f1() (n int) {
	n = 1
	defer func() {
		n++
	}()
	return n
}
func f2() int {
	n := 1
	defer func() {
		n++
	}()
	return n
}

出力:

上記で混乱した場合は、本文に着手しても問題ありません

主な記事

延期とは

deferは、Go言語が提供するメカニズムで、遅延コールを登録することで、一部のリソースが再要求され解放されるようにします。

deferは、現在の関数の実行終了後に実行できる遅延呼び出しを登録します。

登録された関数や式が逆順に実行される場合、先に登録されたものが後に実行されます

以下の例をご覧ください:

package main
import "fmt"
func main() {
 f()
}
func f() {
 defer func() {
 fmt.Println(1)
 }()
 defer func() {
 fmt.Println(2)
 }()
 defer func() {
 fmt.Println(3)
 }()
}

出力:

f, err := os.Open("test.txt")
if err != nil {
 return
}
f.process()
f.Close()

deferの使い方

リソースのリリース

deferを使用することで、リソースリークをある程度回避することができます。特にreturn文が多く、論理エラーによってリソースをクローズし忘れたり、クローズしなかったりしやすいシナリオではそうです。

以下のプログラムは、returnを使用した後、リソースをクローズするステートメントが実行されないため、リソースリークとなります:

f, err := os.Open("test.txt")
if err != nil {
 return
}
defer f.Close()
// ファイルを操作する
f,process()

より良いアプローチは次のようなものです:

func f() {
 defer func() {
 if err := recover(); err != nil {
 fmt.Println(err)
 }
 }()
 // do something
 panic("panic")
}

ここで、プログラムが正常に実行されると、deferはリソースを解放します。deferは、使用前に登録する必要があります。例えば、ここで、ファイルの例外を開く場合、プログラムはreturn文に達すると、deferを経由せずに現在の関数を終了するので、deferはここでは実行されません。

defer 例外のキャッチ

goにはtryとcatchがないので、例外が発生したら、そこから回復する必要があります。defer + recoverで例外をキャッチできます。

func f() {
	if err := recover(); err != nil {
		fmt.Println(err)
	}
	//	do something
	panic("panic")
}

recover()関数は、defer内の無名関数と一緒に呼び出された場合のみ有効であり、以下の手続きでは例外のキャッチはできないことに注意してください:

func trace(msg string) { fmt.Println("entering:", msg) }
func untrace(msg string) { fmt.Println("leaving:", msg) }

コードトレースの実装

以下は、プログラムが関数に入ったとき、あるいは関数から抜けたときの情報を追跡する方法で、特定の関数が実行されたかどうかをテストするために使用できます。

func a() {
	defer un(trace("func a()"))
	fmt.Println("in func a()")
}
func trace(msg string) string {
	fmt.Println("entering:", msg)
	return msg
}
func un(msg string) { fmt.Println("leaving:", msg) }

以下では、この2つの関数の使い方を説明します:

func func1(s string) (n int, err error) {
	defer func() {
		log.Printf("func1(%q) = %d, %v", s, n, err)
	}()
	return 7, nil
}

関数のパラメータと戻り値の文書化

プログラムが期待に沿わない結果を返した場合、デバッグのために手動でログを出力することがあります。 このような場合、deferを使用して関数の引数と戻り値を記録することで、手動で複数の場所にデバッグ文を出力する必要がなくなります。

func f1() (n int) {
	n = 1
	defer func() {
		n++
	}()
	return n
}
func f2() int {
	n := 1
	defer func() {
		n++
	}()
	return n
}

defer

defer と関数の戻り値

状況1:

冒頭の例はこちら:

func f1() (n int) {
	n = 1
	defer func() {
		n++
	}()
	return n
}

ここで、f1() は 2 を返し、f2() は 1 を返します。

return xxx文はアトミック命令ではありません

  1. 戻り値= xxx
  2. defer関数の呼び出し
  3. 空のリターン

これらの点に注意すれば、defer使用後の戻り値に関する問題のほとんどは解決します。

上の2つの例をもう一度見てみましょう:

func f1() (n int) {
	// 1. 戻り値= xxx
	n = 1
	
	// 2. defer関数を呼び出す
	func() {
		n++
	}()
	
	// 3. 空のリターン
	return
}

分解 f1():

func f2() int {
	n := 1
	defer func() {
		n++
	}()
	return n
}

そのため、返されたnは次のように変更されます。

f2()に進みます。

func f2() int {
	n := 1
	// 1. 戻り値= xxx
	匿名の戻り値= n
	// 2. defer関数を呼び出す
	func() {
		n++
	}()
	// 3. 空のリターン
	return
}
func f2() int {
 n := 1
 defer func(n *int) {
 *n++
 }(&n)
 return n
}

ここでdeferによって登録された無名関数はnに対してのみ操作でき、いわゆる無名戻り値に影響を与えることはできません。

そこで質問なのですが、deferによって登録された無名関数がnへのポインタを渡した場合、戻り値に何か影響があるのでしょうか?

func f2() []int {
	n := []int{1}
	defer func() {
		n[0] = 2
	}()
	return n
}

なぜなら、1.戻り値=xxxという処理は値の代入であり、nが変わったからといって戻り値が変わるわけではないからです

しかし、f2()のnがintではなくスライスであった場合、結果は異なり、deferで登録された無名関数は基礎となる配列を変更することができます。

func f3() (n int) {
 n = 1
 defer func(n int) {
 n++
 }(n)
 return n 
}
func f4() (n int) {
 n = 1
 defer func(n *int) {
 *n++
 }(&n)
 return n 
}
type N struct {
	x int
}
func f5() *N {
	n := &N{1}
	defer func() {
		n.x++
	}()
	return n 
}

ここで、 値型と参照型に注意してください。

読者の皆さんには、上記の知識とクロージャに関する知識を踏まえて、以下に挙げるいくつかの例について考えてみていただければと思います:

f3: n = 1
f4: n = 2
f5: n.x = 2

回答

for _, file := range files {
 if f, err = os.Open(file); err != nil {
 return
 }
 // これは間違った方法で、ループが終わってもファイルは閉じられていない
 defer f.Close()
 // ファイルを操作する
 f.Process(data)
}

defer そして

deferをforで使用すると、次の例に示すように、一般的に微妙な問題が発生し、特定のシナリオでは致命的になる可能性があります:

for _, file := range files {
 if f, err = os.Open(file); err != nil {
 return
 }
 // ファイルを操作する
 f.Process(data)
 // ファイルを閉じる
 f.Close()
 }

ループの最後の deferは実行されないのでファイルは閉じられません

Deferは、ループの最後や他の限定されたスコープのコードではなく、関数が戻るときにのみ実行されます。

もっといい方法は

func main() {
 for _, file := range files {
 write(file)
 }
}
func write(file string) {
 ...
 if f, err = os.Open(file); err != nil {
 return
 }
 defer f.Close()
 // ファイルを操作する
 f.Process(data)
}

別の例を見てください:

f, err := os.Open(file)
if err != nil {
 panic(err)
}
defer f.Close()
// ファイルを操作する
f.process()

deferはリソースの解放を遅らせるので、forでは使わないでください

defer ファイルを閉じる

例えば、ファイルを閉じるのを延期する場合、ここに問題があります:

f, err := os.Open(file)
if err != nil {
 panic(err)
}
if f != nil {
 defer f.Close()
}

通常、ファイルを操作した後にdeferを使用してリソースを解放すると、ファイルのオープンに失敗したときにパニックが発生しますが、このケースでは、ファイルのオープンに失敗したときにfがnilになっており、deferを使用してリソースを解放すると新たなパニックが発生します。

より良いアプローチはこうでしょう:

func f() {
	defer func() {
		fmt.Println(1)
	}()
	defer func() {
		fmt.Println(2)
	}()
	panic("panic")
}

defer とパニック

例によって、ここで戻り値を決定する例から始めましょう:

2
1
panic: panic
...

出力:


ここでは、panicが最初にプリントすると思っていましたが、最後にプリントされました。ここで、panicは現在のプロセスを停止し、そこで現在のスレッドのdeferリストにあるステートメントを順次実行することに注意してください。

まとめ

  1. deferは、遅延コールを登録し、一部のリソースが再要求され解放されるようにするためのメカニズムです。
  2. defer 登録された関数は逆の順序で実行されます(最初に登録され、後で実行されます)。
  3. return xxx文はアトミック命令ではないので、deferを使うと戻り値が変わってしまう可能性があります。
  4. deferはリソースの解放を遅らせるので、forでは使わないようにしましょう。
  5. deferは、通常の関数に比べてパフォーマンスが低下します
  6. defer は登録された後に実行されます。

最後に

また、読者の皆様からのご指摘も歓迎いたします。

  • Golangの優しい罠であるDeferは簡単に破られます。

  • ケース

  • パニック脱却

    blog.golang.org/defer-panic...

  • deferの使用例

    github.com/the...

  • go言語のコアプログラミング

Read next

データ構造とアルゴリズム - キューとその関連アルゴリズム

キューの基本\nはじめに\n図に示すように\n\n待ち行列の最後尾だけが入り、先頭だけが出ることができる記憶構造。\n逐次キュー\n\nキューのサイズは固定\nキューアウトとキューインの時間複雑度はO\n連鎖キュー\n実数

Feb 18, 2020 · 6 min read