序章
Goには、次のような他のプログラミング言語に見られる一般的な制御フローキーワードの多くがあります。 if
, switch
, for
、など。 他のほとんどのプログラミング言語にはないキーワードの1つは defer
、そしてあまり一般的ではありませんが、プログラムでどれほど役立つかがすぐにわかります。
の主な用途の1つ defer
ステートメントは、開いているファイル、ネットワーク接続、データベースハンドルなどのリソースをクリーンアップするためのものです。 プログラムがこれらのリソースで終了したら、プログラムの制限を使い果たしないように、また他のプログラムがこれらのリソースにアクセスできるように、リソースを閉じることが重要です。 defer
開いている呼び出しの近くでファイル/リソースを閉じるための呼び出しを維持することにより、コードがよりクリーンになり、エラーが発生しにくくなります。
この記事では、適切に使用する方法を学びます defer
リソースをクリーンアップするためのステートメント、および使用時に行われるいくつかの一般的な間違い defer
.
とは何ですか defer
声明
A defer
ステートメントは、次のfunction呼び出しを追加します defer
スタックへのキーワード。 そのスタック上のすべての呼び出しは、それらが追加された関数が戻ったときに呼び出されます。 呼び出しはスタックに配置されるため、後入れ先出しの順序で呼び出されます。
どのように見てみましょう defer
いくつかのテキストを印刷することによって機能します:
package main
import "fmt"
func main() {
defer fmt.Println("Bye")
fmt.Println("Hi")
}
の中に main
関数には、2つのステートメントがあります。 最初のステートメントは defer
キーワード、その後に print
印刷するステートメント Bye
. 次の行が印刷されます Hi
.
プログラムを実行すると、次の出力が表示されます。
OutputHi
Bye
注意してください Hi
最初に印刷されました。 これは、前にあるステートメントが defer
キーワードは、関数が終了するまで呼び出されません。 defer
使われた。
プログラムをもう一度見てみましょう。今回は、何が起こっているのかを説明するのに役立つコメントを追加します。
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
リソースのクリーンアップを処理するには:
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
:
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
キーワード:
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
、エラーが発生した場合でもエラーを報告します。
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
複数の延期を導入したときに何が起こるかを確認するためのステートメント:
package main
import "fmt"
func main() {
defer fmt.Println("one")
defer fmt.Println("two")
defer fmt.Println("three")
}
プログラムを実行すると、次の出力が表示されます。
Outputthree
two
one
順序は、私たちが呼び出したのとは逆であることに注意してください defer
ステートメント。 これは、呼び出される各遅延ステートメントが前のステートメントの上にスタックされ、関数がスコープを終了するときに逆に呼び出されるためです(後入れ先出し)。
関数では必要な数の遅延呼び出しを行うことができますが、それらはすべて実行されたのとは逆の順序で呼び出されることを覚えておくことが重要です。
複数のdeferが実行される順序がわかったので、複数のdeferを使用して複数のリソースをクリーンアップする方法を見てみましょう。 ファイルを開いて書き込み、もう一度開いて内容を別のファイルにコピーするプログラムを作成します。
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シリーズのコーディング方法全体を調べてください。