defer は golang によって提供されるキーワードで、関数またはメソッドが実行を完了して戻った後に呼び出されます。
各 defer は defer 関数をスタックにプッシュします。関数またはメソッドが呼び出されると、実行のためにスタックから取り出されます。したがって、複数の defer の実行順序は最初から最後です。
for i := 0; i <= 3; i++ { defer fmt.Print(i) } //输出结果时 3,2,1,0
defer トリガーのタイミング
公式 Web サイトでは、次のように明確に説明されています。
「defer」ステートメントは、実行が周囲のタイミングまで延期される関数を呼び出します。周囲の関数が return ステートメントを実行したか、その関数本体の最後に到達したか、または対応するゴルーチンがパニックしているため、関数は戻ります。
- defer ステートメントでラップされた関数が戻るとき
- defer ステートメントでラップされた関数が最後まで実行されたとき
現在の goroutine がパニックになったとき
//输出结果:return前执行defer func f1() { defer fmt.Println("return前执行defer") return } //输出结果:函数执行 // 函数执行到最后 func f2() { defer fmt.Println("函数执行到最后") fmt.Println("函数执行") } //输出结果:panic前 第一个defer在Panic发生时执行,第二个defer在Panic之后声明,不能执行到 func f3() { defer fmt.Println("panic前") panic("panic中") defer fmt.Println("panic后") }
ログイン後にコピー
defer, return,戻り値の実行順序
まずは 3 つの例を見てみましょう
func f1() int { //匿名返回值 var r int = 6 defer func() { r *= 7 }() return r } func f2() (r int) { //有名返回值 defer func() { r *= 7 }() return 6 } func f3() (r int) { //有名返回值 defer func(r int) { r *= 7 }(r) return 6 }
f1 の実行結果は 6、f2 の実行結果は 42、f3 の実行結果は 6
golang の公式ドキュメントでは、return、defer、戻り値の実行順序が紹介されています。
周囲の関数が明示的な return ステートメントを通じて返される場合、遅延関数は、その return ステートメントによって結果パラメーターが設定された後に実行されます。ただし、関数が呼び出し元に戻る前に .
1. 最初に戻り値を割り当てます
2. defer ステートメントを実行します
3 . 関数 return を return
f1 の結果は 6 になります。 f1 は匿名戻り値です。匿名戻り値は return 実行時に宣言されます。したがって、defer が宣言されている場合、匿名戻り値にアクセスできません。defer を変更しても戻り値には影響しません。
f2 は最初に戻り値 r (r=6) を割り当て、defer ステートメントを実行し、defer が r (r = 42) を変更してから、関数が戻ります。
f3 は名前付き戻り値ですが、 r が defer のパラメータとして使用されているため、 defer を宣言するとパラメータがコピーされて渡されるため、 defer は defer 関数のローカル パラメータにのみ影響し、呼び出しには影響しません。関数の戻り値。
クロージャと無名関数
無名関数: 関数名のない関数。
クロージャ: 別の関数のスコープ内で変数を使用できる関数。
for i := 0; i <= 3; i++ { defer func() { fmt.Print(i) } } //输出结果时 3,3,3,3 因为defer函数的i是对for循环i的引用,defer延迟执行,for循环到最后i是3,到defer执行时i就 是3 for i := 0; i <= 3; i++ { defer func(i int) { fmt.Print(i) }(i) } //输出结果时 3,2,1,0 因为defer函数的i是在defer声明的时候,就当作defer参数传递到defer函数中
defer ソース コードの分析
defer の実装ソース コードは runtime.deferproc にあります
次に、関数が戻る前に runtime.deferreturn 関数を実行します。
まず遅延構造を理解します。
type _defer struct { siz int32 started bool sp uintptr // sp at time of defer pc uintptr fn *funcval _panic *_panic // panic that is running defer link *_defer }
sp と pc はそれぞれスタック ポインターと呼び出し元のプログラム カウンターを指します。fn は defer キーワードに渡される関数、Panic は遅延を引き起こすパニックです。実行されます。
defer キーワードが検出されるたびに、defer 関数は runtime.deferproc に変換されます。
deferproc は newdefer を通じて遅延関数を作成し、この新しい遅延関数を現在の goroutine の _defer リンク リストにハングします
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn sp := getcallersp() argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) callerpc := getcallerpc() d := newdefer(siz) if d._panic != nil { throw("deferproc: d.panic != nil after newdefer") } d.fn = fn d.pc = callerpc d.sp = sp switch siz { case 0: // Do nothing. case sys.PtrSize: *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp)) default: memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz)) } return0() }
newdefer は、まず sched と current p の deferpool から _defer 構造体を取り出します。deferpool に _defer がない場合は、新しい _defer が初期化されます。
_defer は現在の g に関連付けられているため、defer は現在の g に対してのみ有効です。
d.link = gp._defer
gp._defer = d //リンク リストを使用して、現在の g のすべての defer を接続します。
func newdefer(siz int32) *_defer { var d *_defer sc := deferclass(uintptr(siz)) gp := getg() if sc < uintptr(len(p{}.deferpool)) { pp := gp.m.p.ptr() if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil { ..... d := sched.deferpool[sc] sched.deferpool[sc] = d.link d.link = nil pp.deferpool[sc] = append(pp.deferpool[sc], d) } if n := len(pp.deferpool[sc]); n > 0 { d = pp.deferpool[sc][n-1] pp.deferpool[sc][n-1] = nil pp.deferpool[sc] = pp.deferpool[sc][:n-1] } } ...... d.siz = siz d.link = gp._defer gp._defer = d return d }
deferreturn 現在の g から _defer リンク リストを取り出し、それを実行すると、_defer を呼び出すたびに、freedefer が _defer 構造体を解放し、_defer 構造体を現在の p の deferpool に置きます。
defer パフォーマンス分析
defer は、開発においてリソースの解放やパニックのキャプチャなどに非常に役立ちます。一部の開発者は、遅延がプログラムのパフォーマンスに及ぼす影響を考慮しておらず、プログラム内で遅延を悪用している可能性があります。
パフォーマンス テストでは、遅延が依然としてパフォーマンスにある程度の影響を与えていることがわかります。 Yuchen の Go パフォーマンス最適化のヒント 4/1 には、defer ステートメントによって生じる追加のオーバーヘッドに関するテストがいくつかあります。
テスト コード
var mu sync.Mutex func noDeferLock() { mu.Lock() mu.Unlock() } func deferLock() { mu.Lock() defer mu.Unlock() } func BenchmarkNoDefer(b *testing.B) { for i := 0; i < b.N; i++ { noDeferLock() } } func BenchmarkDefer(b *testing.B) { for i := 0; i < b.N; i++ { deferLock() }
テスト結果:
BenchmarkNoDefer-4 100000000 11.1 ns/op BenchmarkDefer-4 36367237 33.1 ns/op
前回のソース コード分析から、遅延が発生することがわかります。最初に deferproc を呼び出すと、パラメーターがコピーされ、 deferreturn も関連情報を抽出して実行が遅延します。これらはステートメントを直接呼び出すよりもコストがかかります。
遅延のパフォーマンスは高くありません。各遅延には 20ns かかります。関数内で複数回発生すると、パフォーマンスの消費は 20ns*n になります。CPU リソースの累積的な浪費は非常に大きくなります。
解決策: 例外キャプチャが必要な場合を除き、defer を使用する必要がありますが、その他のリソースリサイクルの延期については、失敗を判断した後に goto を使用してリソースリサイクルのコード領域にジャンプできます。競合リソースについては、使用後すぐにリソースを解放できるため、競合リソースを最適に使用できます。