1 Golangのdefer
機能の紹介
Go言語において、defer
文は、それに続く関数呼び出しの実行を、defer
文を含む関数が実行を終えようとするまで遅延させます。他のプログラミング言語におけるfinally
ブロックのようなものであると考えることができますが、defer
の使用はより柔軟でユニークです。
defer
を使用する利点は、ファイルのクローズ、ミューテックスのアンロック、または単に関数の終了時間の記録などのクリーンアップタスクを実行するために使用できることです。これにより、プログラムはより堅牢になり、例外処理のプログラミング作業量を減らすことができます。Goの設計哲学では、defer
の使用を推奨しており、これによりエラー処理、リソースのクリーンアップ、およびその他の後続の操作を処理する際にコードが簡潔で読みやすくなります。
2 defer
の動作原理
2.1 基本的な動作原理
defer
の基本的な動作原理は、スタック(後入れ先出しの原則)を使用して実行する遅延された関数を保存することです。defer
文が現れると、Go言語はすぐにその後の関数を実行しません。代わりに、それを専用のスタックにプッシュします。外部関数がリターンしようとするときにのみ、これらの遅延された関数がスタックの順序で実行されます。最後に宣言されたdefer
文に続く関数が最初に実行されます。
また、defer
文に続く関数内のパラメータは、実際の実行時ではなく、defer
が宣言された時点で計算および固定されることに注意することが価値があります。
func example() {
defer fmt.Println("world") // 遅延された
fmt.Println("hello")
}
func main() {
example()
}
上記のコードは次のように出力されます:
hello
world
world
はexample
関数が終了する前に出力されますが、コード内でhello
の後に表示されているにもかかわらずです。
2.2 複数のdefer
文の実行順序
関数に複数のdefer
文がある場合、それらは後入れ先出しの順序で実行されます。これは複雑なクリーンアップロジックを理解する際に非常に重要です。次の例は、複数のdefer
文の実行順序を示しています:
func multipleDefers() {
defer fmt.Println("First defer")
defer fmt.Println("Second defer")
defer fmt.Println("Third defer")
fmt.Println("Function body")
}
func main() {
multipleDefers()
}
このコードの出力は次のとおりです:
Function body
Third defer
Second defer
First defer
defer
は後入れ先出しの原則に従うため、「First defer」が最初に遅延されたにもかかわらず、最後に実行されます。
3 異なるシナリオでのdefer
の応用
3.1 リソース解放
Go言語では、defer
文はファイル操作やデータベース接続などのリソース解放ロジックを処理するためによく使用されます。defer
は、関数の実行後に関連するリソースが適切に解放されることを保証します。
ファイル操作の例:
func ReadFile(filename string) {
file, err := os.Open(filename)
if err != nil {
log.Fatal(err)
}
// ファイルが閉じられることを確実にするために、deferを使用
defer file.Close()
// ファイルの読み取り操作を実行...
}
この例では、os.Open
がファイルを正常に開いた場合、その後のdefer file.Close()
文は、関数を抜けるときにファイルリソースが正しく閉じられ、ファイルハンドルリソースが解放されることを保証します。
データベース接続の例:
func QueryDatabase(query string) {
db, err := sql.Open("mysql", "user:password@/dbname")
if err != nil {
log.Fatal(err)
}
// `defer db.Close()`は、関数を抜ける際にデータベース接続が閉じられることを保証します
defer db.Close()
// データベースクエリ操作を実行...
}
同様に、defer db.Close()
は、QueryDatabase
関数を抜ける際にデータベース接続が閉じられるようにします(通常のリターンまたは例外のスローに関係なく)。
3.2 並行プログラミングにおけるロック操作
並行プログラミングでは、defer
を使用してミューテックスロックの解放を処理することが望ましい慣習です。これにより、クリティカルセクションコードの実行後にロックが正しく解放され、デッドロックが発生するのを回避できます。
Mutex Lock の例:
var mutex sync.Mutex
func updateSharedResource() {
mutex.Lock()
// ロックが解除されることを保証するために、defer を使用します
defer mutex.Unlock()
// 共有リソースの変更を行います...
}
共有リソースの変更が成功したか、途中でパニックが発生した場合でも、defer
は Unlock()
の呼び出しを保証し、他のゴルーチンがロックを取得できるようにします。
ヒント: Mutex ロックに関する詳細な説明は後の章でカバーされます。現時点では、defer の適用シナリオを理解することが十分です。
3. defer
の 3 つの一般的な落とし穴と考慮事項
defer
を使用する際、コードの可読性と保守性が大幅に向上しますが、考慮すべき落とし穴と注意事項もあります。
3.1 defer
の遅延関数パラメータは即座に評価されます
func printValue(v int) {
fmt.Println("Value:", v)
}
func main() {
value := 1
defer printValue(value)
// `defer` で渡されたパラメータは変更されない
value = 2
}
// 出力は "Value: 1" となります
defer
ステートメントの後で value
の値が変更されたとしても、defer
で printValue
に渡されたパラメータはすでに評価されて固定されているため、出力は依然として "Value: 1" となります。
3.2 ループ内で defer
を使用する際の注意点
ループ内で defer
を使用すると、ループが終了する前にリソースが解放されない可能性があり、リソースリークや枯渇が発生する可能性があります。
3.3 並行プログラミングでの「使用後解放」を避ける
並行プログラムでは、defer
を使用してリソースを解放する際に、リソースが解放された後もすべてのゴルーチンがリソースにアクセスしようとしないことを確認することが重要です。
4. defer
ステートメントの実行順序に注意する
defer
ステートメントは Last-In-First-Out (LIFO) の原則に従い、最後に宣言された defer
が最初に実行されます。
解決策とベストプラクティス:
-
defer
ステートメントでの関数パラメータは宣言時に評価されることを常に意識すること。 - ループ内で
defer
を使用する際は、匿名関数を使用するか、リソースの明示的な解放を検討すること。 - 並行環境では、全てのゴルーチンが操作を完了してからリソースを解放するために
defer
を使用すること。 - 複数の
defer
ステートメントを含む関数を記述する際には、その実行順序とロジックを慎重に考慮すること。
これらのベストプラクティスに従うことで、defer
の使用時に遭遇する多くの問題を回避し、より堅牢で保守性の高い Go コードを記述できます。