序章

Goには、次のような他のプログラミング言語に見られる一般的な制御フローキーワードの多くがあります。 if, switch, for、など。 他のほとんどのプログラミング言語にはないキーワードの1つは defer、そしてあまり一般的ではありませんが、プログラムでどれほど役立つかがすぐにわかります。

の主な用途の1つ defer ステートメントは、開いているファイル、ネットワーク接続、データベースハンドルなどのリソースをクリーンアップするためのものです。 プログラムがこれらのリソースで終了したら、プログラムの制限を使い果たしないように、また他のプログラムがこれらのリソースにアクセスできるように、リソースを閉じることが重要です。 defer 開いている呼び出しの近くでファイル/リソースを閉じるための呼び出しを維持することにより、コードがよりクリーンになり、エラーが発生しにくくなります。

この記事では、適切に使用する方法を学びます defer リソースをクリーンアップするためのステートメント、および使用時に行われるいくつかの一般的な間違い defer.

とは何ですか defer 声明

A defer ステートメントは、次のfunction呼び出しを追加します defer スタックへのキーワード。 そのスタック上のすべての呼び出しは、それらが追加された関数が戻ったときに呼び出されます。 呼び出しはスタックに配置されるため、後入れ先出しの順序で呼び出されます。

どのように見てみましょう defer いくつかのテキストを印刷することによって機能します:

main.go
package main

import "fmt"

func main() {
	defer fmt.Println("Bye")
	fmt.Println("Hi")
}

の中に main 関数には、2つのステートメントがあります。 最初のステートメントは defer キーワード、その後に print 印刷するステートメント Bye. 次の行が印刷されます Hi.

プログラムを実行すると、次の出力が表示されます。

Output
Hi Bye

注意してください Hi 最初に印刷されました。 これは、前にあるステートメントが defer キーワードは、関数が終了するまで呼び出されません。 defer 使われた。

プログラムをもう一度見てみましょう。今回は、何が起こっているのかを説明するのに役立つコメントを追加します。

main.go
package main

import "fmt"

func main() {
	// defer statement is executed, and places
	// fmt.Println("Bye") on a list to be executed prior to the function returning
	defer fmt.Println("Bye")

	// The next line is executed immediately
	fmt.Println("Hi")

	// fmt.Println*("Bye") is now invoked, as we are at the end of the function scope
}

理解するための鍵 defer それは defer ステートメントが実行されると、遅延関数への引数がすぐに評価されます。 いつ defer 実行すると、関数が戻る前に呼び出されるリストに、それに続くステートメントが配置されます。

このコードは順序を示していますが defer 実行されますが、Goプログラムを作成するときに使用される一般的な方法ではありません。 使用している可能性が高い defer ファイルハンドルなどのリソースをクリーンアップします。 次にそれを行う方法を見てみましょう。

使用する defer リソースをクリーンアップする

使用する defer Goでは、リソースをクリーンアップすることは非常に一般的です。 まず、ファイルに文字列を書き込むが、使用しないプログラムを見てみましょう。 defer リソースのクリーンアップを処理するには:

main.go
package main

import (
	"io"
	"log"
	"os"
)

func main() {
	if err := write("readme.txt", "This is a readme file"); err != nil {
		log.Fatal("failed to write file:", err)
	}
}

func write(fileName string, text string) error {
	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	_, err = io.WriteString(file, text)
	if err != nil {
		return err
	}
	file.Close()
	return nil
}

このプログラムには、という関数があります write 最初にファイルの作成を試みます。 エラーがある場合は、エラーを返し、関数を終了します。 次に、文字列を書き込もうとします This is a readme file 指定されたファイルに。 エラーを受け取った場合は、エラーを返し、関数を終了します。 次に、関数はファイルを閉じて、リソースをシステムに解放しようとします。 最後に、関数は戻ります nil 関数がエラーなしで実行されたことを示します。

このコードは機能しますが、微妙なバグがあります。 に電話する場合 io.WriteString 失敗した場合、関数はファイルを閉じずにリソースをシステムに解放せずに戻ります。

別のものを追加することで問題を解決できます file.Close() ステートメント、これはあなたがなしの言語でこれを解決する可能性が高い方法です defer:

main.go
package main

import (
	"io"
	"log"
	"os"
)

func main() {
	if err := write("readme.txt", "This is a readme file"); err != nil {
		log.Fatal("failed to write file:", err)
	}
}

func write(fileName string, text string) error {
	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	_, err = io.WriteString(file, text)
	if err != nil {
		file.Close()
		return err
	}
	file.Close()
	return nil
}

今でも io.WriteString 失敗しても、ファイルを閉じます。 これは比較的簡単に見つけて修正できるバグでしたが、より複雑な機能を備えていたため、見逃されていた可能性があります。

に2番目の呼び出しを追加する代わりに file.Close()、使用できます defer 実行中にどのブランチが実行されるかに関係なく、常に Close().

これがを使用するバージョンです defer キーワード:

main.go
package main

import (
	"io"
	"log"
	"os"
)

func main() {
	if err := write("readme.txt", "This is a readme file"); err != nil {
		log.Fatal("failed to write file:", err)
	}
}

func write(fileName string, text string) error {
	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = io.WriteString(file, text)
	if err != nil {
		return err
	}
	return nil
}

今回は、次のコード行を追加しました。 defer file.Close(). これは、コンパイラに実行する必要があることを通知します。 file.Close 関数を終了する前に write.

これで、コードを追加して、将来関数を終了する別のブランチを作成した場合でも、常にファイルをクリーンアップして閉じることが保証されました。

ただし、延期を追加することにより、さらに別のバグを導入しました。 から返される可能性のある潜在的なエラーをチェックしなくなりました Close 方法。 これは私たちが使うとき defer、戻り値を関数に返す方法はありません。

Goでは、電話をかけることは安全で受け入れられている慣行と見なされています Close() プログラムの動作に影響を与えることなく、複数回。 もしも Close() エラーを返す場合は、最初に呼び出されたときに返されます。 これにより、関数の実行の成功パスで明示的に呼び出すことができます。

両方ができる方法を見てみましょう defer への呼び出し Close、エラーが発生した場合でもエラーを報告します。

main.go
package main

import (
	"io"
	"log"
	"os"
)

func main() {
	if err := write("readme.txt", "This is a readme file"); err != nil {
		log.Fatal("failed to write file:", err)
	}
}

func write(fileName string, text string) error {
	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = io.WriteString(file, text)
	if err != nil {
		return err
	}

	return file.Close()
}

このプログラムの唯一の変更は、私たちが返す最後の行です file.Close(). に電話する場合 Close 結果としてエラーが発生します。これは、呼び出し元の関数に期待どおりに返されます。 私たちのことを覚えておいてください defer file.Close() ステートメントは、 return 声明。 この意味は file.Close() 潜在的に2回呼び出されます。 これは理想的ではありませんが、プログラムに副作用を引き起こさないため、許容できる方法です。

ただし、関数を呼び出すときなど、関数の早い段階でエラーを受け取った場合 WriteString、関数はそのエラーを返し、呼び出しを試みます file.Close 延期されたからです。 それでも file.Close エラーも返される可能性があります(おそらく返される可能性があります)。最初に何が悪かったのかを示す可能性が高いエラーを受け取ったため、これはもはや気にすることではありません。

これまで、シングルを使用する方法を見てきました defer リソースを適切にクリーンアップするため。 次に、複数を使用する方法を見ていきます defer 複数のリソースをクリーンアップするためのステートメント。

多数 defer ステートメント

複数あるのが普通です defer 関数内のステートメント。 だけを持っているプログラムを作成しましょう defer 複数の延期を導入したときに何が起こるかを確認するためのステートメント:

main.go
package main

import "fmt"

func main() {
	defer fmt.Println("one")
	defer fmt.Println("two")
	defer fmt.Println("three")
}

プログラムを実行すると、次の出力が表示されます。

Output
three two one

順序は、私たちが呼び出したのとは逆であることに注意してください defer ステートメント。 これは、呼び出される各遅延ステートメントが前のステートメントの上にスタックされ、関数がスコープを終了するときに逆に呼び出されるためです(後入れ先出し)。

関数では必要な数の遅延呼び出しを行うことができますが、それらはすべて実行されたのとは逆の順序で呼び出されることを覚えておくことが重要です。

複数のdeferが実行される順序がわかったので、複数のdeferを使用して複数のリソースをクリーンアップする方法を見てみましょう。 ファイルを開いて書き込み、もう一度開いて内容を別のファイルにコピーするプログラムを作成します。

main.go
package main

import (
	"fmt"
	"io"
	"log"
	"os"
)

func main() {
	if err := write("sample.txt", "This file contains some sample text."); err != nil {
		log.Fatal("failed to create file")
	}

	if err := fileCopy("sample.txt", "sample-copy.txt"); err != nil {
		log.Fatal("failed to copy file: %s")
	}
}

func write(fileName string, text string) error {
	file, err := os.Create(fileName)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = io.WriteString(file, text)
	if err != nil {
		return err
	}

	return file.Close()
}

func fileCopy(source string, destination string) error {
	src, err := os.Open(source)
	if err != nil {
		return err
	}
	defer src.Close()

	dst, err := os.Create(destination)
	if err != nil {
		return err
	}
	defer dst.Close()

	n, err := io.Copy(dst, src)
	if err != nil {
		return err
	}
	fmt.Printf("Copied %d bytes from %s to %s\n", n, source, destination)

	if err := src.Close(); err != nil {
		return err
	}

	return dst.Close()
}

と呼ばれる新しい関数を追加しました fileCopy. この関数では、最初にコピー元のソースファイルを開きます。 ファイルを開くときにエラーが発生したかどうかを確認します。 もしそうなら、私たちは return エラーが発生し、関数を終了します。 そうでなければ、私たちは defer 開いたばかりのソースファイルを閉じます。

次に、宛先ファイルを作成します。 ここでも、ファイルの作成中にエラーが発生したかどうかを確認します。 もしそうなら、私たちは return そのエラーと関数を終了します。 そうでなければ、私たちも defer the Close() 宛先ファイル用。 今は2つあります defer 関数がスコープを終了するときに呼び出される関数。

両方のファイルを開いたので、次のようにします Copy() ソースファイルから宛先ファイルへのデータ。 それが成功した場合、両方のファイルを閉じようとします。 いずれかのファイルを閉じようとしてエラーが発生した場合は、 return エラーおよび終了関数スコープ。

明示的に呼び出すことに注意してください Close() ファイルごとに、 defer また呼び出す Close(). これは、ファイルを閉じるときにエラーが発生した場合に、エラーを報告するためです。 また、何らかの理由で関数がエラーで早期に終了した場合、たとえば2つのファイル間でコピーに失敗した場合でも、各ファイルは遅延呼び出しから適切に閉じようとします。

結論

この記事では、 defer ステートメント、およびプログラム内のシステムリソースを適切にクリーンアップするためにそれをどのように使用できるか。 システムリソースを適切にクリーンアップすると、プログラムのメモリ使用量が減り、パフォーマンスが向上します。 どこについてもっと学ぶために defer を使用するか、パニックの処理に関する記事を読むか、Goシリーズのコーディング方法全体を調べてください。