Goは静的型付けされた、並行処理が可能で効率的なプログラミング言語で、近年多くの人気を集めています。Goの最も興味深い特徴の1つは、周囲の関数が完了するまで関数の実行を延期することができるdeferキーワードです。
deferは、関数の最後で実行するべき処理を遅延させるために使用します。deferを使用すると、関数が正常に終了した時やpanicによるエラーが発生した時でも、deferに登録した処理が最後に必ず実行されます。deferキーワードは、ファイルのクローズやロックの解放など、リソースの後始末によく使われます。
ファイルを開く/閉じるという操作を例にdeferについて考えてみましょう。
例えば、ファイルの読み込み処理を行う場合、一度そのファイルを開くという操作を行いますが、その際にOSは必要なメモリ領域を確保します。その後、読み込み処理が完了し、ファイルを使い終わるとファイルを閉じ、確保したメモリ領域を解放します。
ここでもし、ファイルを閉じる操作を怠ると、ファイルが開かれるたびにメモリを消費していき、やがてメモリリークが発生してしまいます。従って、ファイルを開くという操作の後には必ずそのファイルを閉じなければなりません。
それではファイルの読み込み処理を行う処理を見てみましょう。
file, err := os.Open("file.txt")
if err != nil {
log.Fatal(err)
}
data, err := io.ReadAll(file)
if err != nil {
log.Fatal(err)
}
err = file.Close()
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
ファイルfile.txtをos.Open関数で開き、ファイルポインタとエラーを変数fileとerrにそれぞれ代入しています。ファイルを開くのにエラーがあれば、そのエラーをログに記録し、log.Fatal(err)を使ってプログラムを終了させます。
io.ReadAll関数を使用して、ファイルから変数dataにすべてのデータを読み込んでいます。もしデータの読み込みにエラーがあれば、そのエラーをログに記録し、log.Fatal(err)を使用してプログラムを終了します。
file.Closeメソッドでファイルを閉じます。ファイルを閉じる際にエラーが発生した場合、エラーをログに記録し、log.Fatal(err)を使用してプログラムを終了します。
fmt.Println(string(data))でファイルの内容を文字列として出力しています。
この場合、ファイルの内容を読み込んだ後、手動でファイルを閉じる必要があります。このコードの後に再度ファイルの読み込みを行う処理を記述すると、再度その処理の後に手動でファイルを閉じる処理を書く必要があり、コードが煩雑になっていきます。
それでは、deferキーワードを使ってプログラムを書き直してみましょう。
file, err := os.Open("file.txt")
if err != nil {
log.Fatal(err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
log.Fatal(err)
}
fmt.Println(string(data))
deferキーワードを使用して、file.Closeメソッドの実行を周囲の関数が完了するまで延期します。これにより、関数の実行中にエラーが発生しても、ファイルは自動的に閉じられます。この例ではdeferを使っているので、複数の場所で手動でファイルを閉じる必要がなく、コードがすっきりし、エラーが起こりにくくなります。
ファイルを再度読み込む処理を追加しても、deferキーワードのおかげでファイルを閉じる処理を再度記述する必要はありません。file.Close関数をdeferに登録することで、処理結果に関わらず、file.Close関数を最後に必ず実行するように指定しています。
deferを使用することで、コードを単純化し、より堅牢にすることができます。これは、関数にエラーが発生したり予期せぬ終了があった場合でも、特定の操作が実行されるようにするためです。
deferキーワードは、LIFO(Last In First Out)順で評価されることに注意することが重要です。つまり、関数内の最後のdefer文が、関数が戻ったときに最初に実行されることになります。これは、複数のロックを正しい順序で解放する場合など、リソースを逆順にクリーンアップしたい場合に便利です。
それでは例を見てみましょう。
f1, err1 := os.Open("example1.txt")
defer f1.Close()
f2, err2 := os.Open("example2.txt")
defer f2.Close()
上の例では、f1.Close()、f2.Close()の順番でdeferによる登録が行われていますが、deferによる登録はスタック構造になっているため、deferの実行は最後に書いたdeferの順f2.Close()、f1.Close()となります。
並行処理におけるロック/アンロックの例も見てみましょう。
func processData(data []int, lock1, lock2 *sync.Mutex) []int {
lock1.Lock()
defer lock1.Unlock()
lock2.Lock()
defer lock2.Unlock()
for i := range data {
data[i]++
}
return data
}
deferのもう一つの興味深い点は、関数の実行時ではなく、defer文の実行時に遅延した関数に引数を渡すことができる点です。これは、関数が実行されるときに引数が渡される通常の関数呼び出しとは異なります。変数の値が変化した場合でも、現在の状態を把握しておいて後で使いたいような場合に便利です。
package main
import (
"fmt"
)
func processData(data []int) []int {
defer func(data []int) {
for i := range data {
fmt.Println("Data at index", i, "is", data[i])
}
}(data)
for i := range data {
fmt.Println(i)
data[i]++
}
return data
}
func main() {
data := []int{1, 2, 3, 4, 5}
processData(data)
}
// 0
// 1
// 2
// 3
// 4
// Data at index 0 is 2
// Data at index 1 is 3
// Data at index 2 is 4
// Data at index 3 is 5
// Data at index 4 is 6
この例では、defer文を使用して、各インデックスiでdata[i]の値を表示しています。引数dataは、関数が実行されるときではなく、defer文が実行されるときに、遅延関数に渡されます。つまり、iの値が関数の後半で変化しても、defer文の実行時に取り込まれたiの値が使われることになります。
その結果、data[i]の値は、関数内で何らかの処理が行われる前の元の値が出力されることになります。このように、defer文を使うと、変数の値が変わっても、その変数の現在の状態を取得し、後で使用することができます。