defer
Kata kunci defer adalah kata kunci yang sangat sering muncul dalam pengembangan go sehari-hari, ia akan mengeksekusi fungsi yang terkait dengan defer dalam urutan LIFO (Last In First Out), seringkali kita memanfaatkan mekanisme ini untuk melakukan operasi pelepasan sumber daya, seperti operasi penutupan file.
fd, err := os.Open("/dev/stdin")
if err != nil{
return err
}
defer fd.Close()
...Kata kunci yang sering muncul seperti ini membuat kita perlu memahami struktur di baliknya.
Struktur
Kata kunci defer sesuai dengan struktur runtime._defer, strukturnya tidak terlalu kompleks
type _defer struct {
started bool
heap bool
openDefer bool
sp uintptr // sp pada saat defer
pc uintptr // pc pada saat defer
fn func() // bisa nil untuk defer open-coded
_panic *_panic // panic yang sedang menjalankan defer
link *_defer // defer berikutnya pada G; bisa menunjuk ke heap atau stack!
fd unsafe.Pointer // funcdata untuk fungsi yang terkait dengan frame
varp uintptr // nilai varp untuk stack frame
framepc uintptr
}Field fn di antaranya adalah fungsi yang sesuai dengan kata kunci defer, link menunjukkan defer berikutnya, sp dan pc mencatat informasi fungsi pemanggil, digunakan untuk menentukan defer milik fungsi mana. defer ada dalam bentuk linked list pada runtime, kepala linked list ada di goroutine G, jadi defer sebenarnya terkait langsung dengan goroutine.
type g struct {
...
_panic *_panic // panic terdalam - offset diketahui oleh liblink
_defer *_defer // defer terdalam
...
}Ketika goroutine mengeksekusi fungsi, defer dalam fungsi akan ditambahkan ke kepala linked list secara berurutan
defer fn1()
defer fn2()
defer fn3()Kode di atas sesuai dengan gambar berikut

Selain goroutine, P juga memiliki hubungan tertentu dengan defer, dalam struktur P, ada field deferpool, seperti yang ditunjukkan.
type p struct {
...
deferpool []*_defer // pool struktur defer yang tersedia (lihat panic.go)
deferpoolbuf [32]*_defer
...
}Di deferpool存放的是预分配好的defer结构,用于给与 P 关联的 goroutine G 分配新的defer结构,可以减少开销。
Alokasi
Dalam sintaks penggunaan kata kunci defer, compiler akan mengubahnya menjadi pemanggilan fungsi runtime.deferproc. Misalnya kode go seperti ini
defer fn1(x, y)Sedangkan setelah dikompilasi kode sebenarnya seperti ini
deferproc(func(){
fn1(x, y)
})Jadi sebenarnya fungsi yang传入 defer tidak memiliki parameter dan tidak memiliki nilai return, kode fungsi deferproc adalah sebagai berikut
func deferproc(fn func()) {
gp := getg()
d := newdefer()
d.link = gp._defer
gp._defer = d
d.fn = fn
d.pc = getcallerpc()
d.sp = getcallersp()
return0()
}Fungsi ini bertanggung jawab untuk membuat struktur defer dan menambahkannya ke kepala linked list goroutine G, fungsi runtime.newdefer di antaranya akan mencoba mendapatkan struktur defer yang sudah dialokasikan sebelumnya dari deferpool di P.
if len(pp.deferpool) == 0 && sched.deferpool != nil {
lock(&sched.deferlock)
for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {
d := sched.deferpool
sched.deferpool = d.link
d.link = nil
pp.deferpool = append(pp.deferpool, d)
}
unlock(&sched.deferlock)
}Pertama-tama ia akan mengisi setengah defer dari sched.deferpool global ke deferpool lokal, kemudian mencoba mendapatkan dari deferpool di P
if n := len(pp.deferpool); n > 0 {
d = pp.deferpool[n-1]
pp.deferpool[n-1] = nil
pp.deferpool = pp.deferpool[:n-1]
}
if d == nil {
// Alokasikan defer baru.
d = new(_defer)
}
d.heap = trueTerakhir jika benar-benar tidak ditemukan baru akan menggunakan cara alokasi manual. Terakhir dapat dilihat ada kode seperti ini
d.heap = trueIni menunjukkan defer dialokasikan di heap, sesuai dengan ketika false, akan dialokasikan di stack, memori yang dialokasikan di stack akan otomatis dikembalikan saat return, efisiensi manajemen memorinya lebih tinggi daripada di heap, dan faktor yang menentukan apakah dialokasikan di stack adalah jumlah lapisan loop, bagian logika ini dapat ditelusuri ke metode escape.goDeferStmt di cmd/compile/ssagen seperti berikut
func (e *escape) goDeferStmt(n *ir.GoDeferStmt) {
...
if n.Op() == ir.ODEFER && e.loopDepth == 1 {
k = e.later(e.discardHole())
n.SetEsc(ir.EscNever)
}
...
}e.loopDepth menunjukkan jumlah lapisan loop untuk pernyataan saat ini, jika pernyataan defer saat ini tidak berada dalam loop, akan dialokasikan di stack.
case ir.ODEFER:
n := n.(*ir.GoDeferStmt)
if s.hasOpenDefers {
s.openDeferRecord(n.Call.(*ir.CallExpr))
} else {
d := callDefer
if n.Esc() == ir.EscNever {
d = callDeferStack
}
s.callResult(n.Call.(*ir.CallExpr), d)
}Jika dialokasikan di stack, akan langsung membuat struktur defer di stack, akhirnya akan diselesaikan oleh fungsi runtime.deferprocStack untuk membuat struktur defer.
if k == callDeferStack {
// Buat struktur defer d di stack.
if stksize != 0 {
s.Fatalf("deferprocStack dengan ukuran stack non-nol %d: %v", stksize, n)
}
t := deferstruct()
...
// Panggil runtime.deferprocStack dengan pointer ke record *_defer.
ACArgs = append(ACArgs, types.Types[types.TUINTPTR])
aux := ssa.StaticAuxCall(ir.Syms.DeferprocStack, s.f.ABIDefault.ABIAnalyzeTypes(nil, ACArgs, ACResults))
callArgs = append(callArgs, addr, s.mem())
call = s.newValue0A(ssa.OpStaticLECall, aux.LateExpansionResultType(), aux)
call.AddArgs(callArgs...)
call.AuxInt = int64(types.PtrSize) // deferprocStack mengambil argumen *_deferSignature fungsi deferprocStack adalah sebagai berikut
func deferprocStack(d *_defer)Logika pembuatan spesifiknya tidak terlalu berbeda dengan deferproc, perbedaan utamanya adalah, saat alokasi di stack sumber struktur defer adalah struktur yang dibuat langsung, sumber defer yang dialokasikan di heap adalah fungsi new.
Eksekusi
Ketika fungsi akan return atau terjadi panic, akan masuk ke fungsi runtime.deferreturn, fungsi ini bertanggung jawab untuk mengambil defer dari linked list goroutine dan mengeksekusinya.
func deferreturn() {
gp := getg()
for {
d := gp._defer
sp := getcallersp()
if d.sp != sp {
return
}
fn := d.fn
d.fn = nil
gp._defer = d.link
freedefer(d)
fn()
}
}Pertama-tama akan mendapatkan stack frame fungsi saat ini melalui getcallersp() dan membandingkannya dengan sp dalam struktur defer untuk menentukan apakah defer milik fungsi saat ini, kemudian mengambil struktur defer dari kepala linked list, dan menggunakan gp._defer = d.link untuk mengeksekusi defer berikutnya, kemudian melepaskan struktur defer kembali ke pool melalui fungsi runtuime.freedefer, terakhir memanggil fn untuk mengeksekusi, terus berulang seperti ini sampai selesai mengeksekusi semua defer milik fungsi saat ini.
Open-coded
Penggunaan defer bukan tanpa biaya, meskipun dalam sintaks memberikan kemudahan bagi kita, tetapi bagaimanapun juga bukan pemanggilan fungsi langsung, di antaranya akan melalui serangkaian proses, jadi tetap akan menyebabkan kerugian performa, jadi kemudian go resmi merancang sebuah cara optimasi——open-coded, ini adalah cara optimasi untuk defer, nama Inggris aslinya adalah open-coded, di dalam negeri pada dasarnya diterjemahkan menjadi open-coded, open di sini berarti展开,yaitu展开 kode fungsi defer ke dalam kode fungsi saat ini, seperti fungsi inlining. Cara optimasi ini memiliki beberapa kondisi pembatas berikut
- Jumlah
deferdalam fungsi tidak boleh melebihi 8 - Hasil kali jumlah
deferdanreturntidak boleh melebihi 15 defertidak boleh muncul dalam loop- Optimasi kompilasi tidak dinonaktifkan
- Tidak secara manual memanggil
os.Exit() - Tidak perlu menyalin parameter dari heap
Bagian logika判断 ini dapat ditelusuri ke bagian kode berikut dari fungsi cmd/compile/ssagen.buildssa
s.hasOpenDefers = base.Flag.N == 0 && s.hasdefer && !s.curfn.OpenCodedDeferDisallowed()
if s.hasOpenDefers && len(s.curfn.Exit) > 0 {
s.hasOpenDefers = false
}
if s.hasOpenDefers {
for _, f := range s.curfn.Type().Results().FieldSlice() {
if !f.Nname.(*ir.Name).OnStack() {
s.hasOpenDefers = false
break
}
}
}
if s.hasOpenDefers && s.curfn.NumReturns*s.curfn.NumDefers > 15 {
s.hasOpenDefers = false
}Kemudian go akan membuat variabel integer 8-bit deferBits dalam fungsi saat ini untuk digunakan sebagai bitmap untuk menandai defer, setiap bit menandai satu, integer 8-bit uint8 paling banyak dapat表示 8, jika bit yang sesuai adalah 1, maka defer yang dioptimalkan dengan open-coded akan dieksekusi saat fungsi akan return.
