Goでは、キーワードdeferは、周囲の関数の終了まで関数の実行を遅延させます。これは、リソースを解放したり、終了処理を行うのに便利です。しかし、ポインタ(*T)や値(T)を受け取るメソッドとdeferを組み合わせて使用する際には、明白ではない微妙な点があります。
Goはコードの安全性から、関数からのすべての出口でクリーンアップコードを自動的に呼び出すことを保証するように設計されています。しかし、使用されるレシーバのタイプ(ポインタまたは値)が、deferを呼び出すメソッドのオブジェクトがコピーされるか、元のオブジェクトが変更されるかを決定します。
レシーバが値(T)の場合、deferでは構造体の値がコピーされます。レシーバがポインタ(*T)の場合、メソッドは元のオブジェクトで動作します。その結果、deferメソッド内での値の変更は無視され、ポインタの場合は外部の構造体に反映されます。これは、特にdeferを通じてオブジェクトの状態を変更しようとする際に、追跡が難しいエラーを引き起こします。
メソッドを設計し、deferを使用する際には、常にレシーバのタイプを意識して選択する必要があります。関数の最後まで生き残るべき変更は、ポインタを介して行うべきです。
コード例:
package main import "fmt" type Counter struct { Value int } func (c Counter) IncValue() { // 値受信者メソッド defer func() { c.Value++ // コピーだけが増加する fmt.Println("[Value receiver defer] Value:", c.Value) }() } func (c *Counter) IncPointer() { // ポインタ受信者メソッド defer func() { c.Value++ // オリジナルが増加する fmt.Println("[Pointer receiver defer] Value:", c.Value) }() } func main() { c := Counter{Value: 10} c.IncValue() // Valueは10のまま c.IncPointer() // Valueは11になる fmt.Println("Original Value after calls:", c.Value) }
重要なポイント:
1. 値受信者によるメソッドをdeferで呼び出した場合、外部オブジェクトは変更されますか?
いいえ、defer内では元のオブジェクトは変更されず、メソッドは構造体のコピーで動作します。
2. 値受信者によるメソッドを通じて構造体の状態を変更することを保証するためにdeferを信頼できますか?
いいえ。元のオブジェクトに変更を反映させたい場合は、ポインタメソッドを使用する必要があります。
3. deferの宣言後に構造体のフィールドを変更した場合どうなりますか?
動作はレシーバの渡し方によって異なります。メソッド/関数がコピーを受け取る場合は、deferの宣言後の変更は遅延関数では見えなくなります。
func (c Counter) Demo() { defer fmt.Println("defer c.Value:", c.Value) // 現在の値をキャプチャする c.Value = 42 // deferには影響しない }
値受信者のメソッドを使ってdeferで状態を更新する接続終了関数を作成しました。結果として、終了フラグが更新されませんでした。
利点:
欠点:
ポインタ受信者のメソッドを使用して終了処理を行い、元のオブジェクトを変更しました。
利点:
欠点: