Pernyataan defer menjadwalkan sebuah pemanggilan fungsi untuk dijalankan tepat sebelum fungsi yang mengelilinginya selesai. Topik ini membahas mekanisme defer, urutan eksekusi pemanggilan yang ditunda, evaluasi argumen, serta idiom pembersihan sumber daya yang menjadi alasan utama keberadaan defer. Setelah membacanya, pemanggilan pembersihan dapat ditempatkan dengan benar sehingga tidak terlewat pada fungsi yang memiliki banyak jalur keluar.
Cara kerja defer dan urutannya#
Setiap pernyataan defer menunda pemanggilan yang ditulisnya, tetapi tidak mengeksekusinya langsung. Pemanggilan yang ditunda didaftarkan ke dalam tumpukan milik fungsi pengelilingnya dan dijalankan berurutan setelah badan fungsi selesai, tepat sebelum kendali kembali kepada pemanggil. Program berikut memuat tiga defer beserta dua baris pencetakan biasa.
package main
import "fmt"
func main() {
fmt.Println("mulai")
defer fmt.Println("pertama")
defer fmt.Println("kedua")
defer fmt.Println("ketiga")
fmt.Println("selesai")
}mulai
selesai
ketiga
kedua
pertamaUrutan keluarannya terbalik terhadap urutan penulisan, yaitu LIFO (last in, first out, terakhir masuk pertama keluar). Urutan ini masuk akal karena sumber daya yang dibuka belakangan sering bergantung pada sumber daya yang dibuka lebih dahulu, sehingga pembersihan sumber daya yang belakangan wajib berjalan lebih dahulu. Sifat ini menjadikan defer andal sekalipun fungsi keluar melalui pernyataan return yang berbeda-beda.
Lingkup defer adalah fungsi, bukan blok. Pernyataan defer yang tertulis di dalam if atau for tetap menunggu fungsi pengelilingnya selesai, bukan selesainya blok tersebut. Setiap pemanggilan yang ditunda juga menumpuk di memori hingga fungsi berakhir, sehingga jumlah defer pada satu fungsi lazimnya dijaga tetap kecil.
Evaluasi argumen saat pernyataan defer#
Argumen fungsi yang ditunda dievaluasi pada saat pernyataan defer ditulis, bukan pada saat pemanggilannya akhirnya berjalan. Nilai yang berubah setelah baris defer tidak memengaruhi argumen yang sudah terkunci tersebut. Perilaku ini berbeda dengan closure, dan program berikut menampilkan keduanya berdampingan.
package main
import "fmt"
func main() {
suhu := 24
defer fmt.Println("argumen dievaluasi saat defer: suhu", suhu)
defer func() {
fmt.Println("closure membaca variabel saat eksekusi: suhu", suhu)
}()
suhu = 30
fmt.Println("badan fungsi selesai: suhu", suhu)
}badan fungsi selesai: suhu 30
closure membaca variabel saat eksekusi: suhu 30
argumen dievaluasi saat defer: suhu 24Baris pertama defer mengunci nilai 24 sebab argumennya dievaluasi seketika. Baris kedua defer membungkus fungsi tanpa nama yang menangkap variabel suhu, sehingga saat eksekusinya berjalan, nilai yang terbaca adalah 30. Dengan begitu, bentuk defer fmt.Println(nilai) dan bentuk defer func() { fmt.Println(nilai) }() memakai nilai yang berbeda walaupun tampak serupa.
Defer dan hasil bernama#
Fungsi yang ditunda dapat membaca hasil bernama fungsi pengelilingnya, bahkan menyuntingnya. Mekanismenya berjalan berurutan, yaitu pernyataan return mengisi hasil bernama terlebih dahulu, fungsi yang ditunda berjalan setelahnya, dan nilai terakhir itulah yang diterima pemanggil. Dengan begitu, fungsi tertunda dapat menambahkan sentuhan terakhir pada nilai kembalian.
package main
import "fmt"
// tandaSelesai menambahkan penanda pada hasil kembalian lewat fungsi
// tertunda yang membaca hasil bernama.
func tandaSelesai(nilai int) (hasil string) {
defer func() {
hasil += " (sumber daya ditutup)"
}()
if nilai < 0 {
return "negatif"
}
return fmt.Sprintf("nilai %d", nilai)
}
func main() {
fmt.Println(tandaSelesai(24))
fmt.Println(tandaSelesai(-5))
}nilai 24 (sumber daya ditutup)
negatif (sumber daya ditutup)Penanda (sumber daya ditutup) ditambahkan oleh fungsi tertunda pada kedua jalur keluar tanpa perlu ditulis ulang di setiap pernyataan return. Pola ini sama dengan yang dipakai untuk mengubah panic menjadi error, yaitu fungsi tertunda mengisi hasil bernama err. Bentuknya berguna ketika sentuhan akhir harus berlaku untuk semua jalur keluar sekaligus.
Idiom pasangan buka dan tutup#
Pemakaian defer yang paling lazim adalah menutup sumber daya tepat setelah sumber daya itu berhasil diperoleh. Pola ini menjamin pemanggilan penutup berjalan pada semua jalur keluar fungsi, termasuk keluar lebih awal karena kegagalan. Program berikut meniru pasangan buka dan tutup tersebut tanpa berkas sungguhan.
package main
import "fmt"
// bacaCatatan meniru pasangan buka dan tutup pada satu sumber daya.
func bacaCatatan() {
fmt.Println("buka: catatan-sensor.txt")
defer fmt.Println("tutup: catatan-sensor.txt")
fmt.Println("baca: baris pertama")
}
func main() {
bacaCatatan()
fmt.Println("fungsi utama berlanjut")
}buka: catatan-sensor.txt
baca: baris pertama
tutup: catatan-sensor.txt
fungsi utama berlanjutPada program nyata, pasangan buka dan tutup itu berupa pembukaan berkas dengan os.Open dan penutupannya dengan Close. Bentuk idiomnya terlihat sebagai berikut.
// tidak-dijalankan
berkas, err := os.Open("catatan-sensor.txt")
if err != nil {
return err
}
defer berkas.Close()
// pemrosesan isi berkas diletakkan di bawah ini
// Close tetap terpanggil sekalipun fungsi keluar lebih awalPemeriksaan error diletakkan sebelum defer supaya penutupan hanya dijadwalkan ketika berkas benar-benar terbuka. Penempatan ini juga membuat sumber daya dan pembersihnya berdampingan dalam dua baris, sehingga pasangan itu tidak mudah terpisah saat kode disunting.
Satu catatan menyertai idiom ini, yaitu Close sendiri dapat mengembalikan error. Pada pembacaan berkas, error dari Close lazim dibiarkan tanpa diperiksa karena kegagalannya tidak merusak hasil bacaan. Pada penulisan berkas, error tersebut perlu diperiksa karena data yang tertunda di penyangga dapat gagal tersimpan ketika berkas ditutup, dan pemeriksaannya dilakukan terpisah dari defer.
Pola pasangan ini berlaku untuk sumber daya lain, misalnya koneksi jaringan dan pengunci (lock) yang dilepas dengan Unlock. Prinsipnya satu, yaitu memanggil pembersih tepat setelah pemerolehan berhasil, sehingga kedua barisnya berdampingan dan urutannya tidak tertukar. Fungsi yang memegang lebih dari satu sumber daya menumpuk beberapa defer, dan urutan pembersihannya terbalik dari urutan pemerolehannya sebagaimana tuntutan LIFO.
Kesalahan umum#
- Meletakkan
deferdi dalam perulangan. Setiap defer menunggu fungsi pengelilingnya selesai, sehingga seribu iterasi yang membuka berkas menumpuk seribu penutupan hingga fungsi selesai dan bisa menghabiskan kuota file descriptor. Perbaikannya, pindahkan tubuh perulangan ke fungsi tersendiri agar defer berlaku per iterasi. - Mengira argumen defer dievaluasi saat eksekusinya berjalan. Nilai yang dijadwalkan adalah nilai pada baris defer, sehingga perubahan belakangan tidak terbaca dan penyelidikan menjadi membingungkan. Perbaikannya, bungkus pembacaan nilai dalam fungsi tanpa nama agar nilai terbarunya yang diambil.
- Mengira defer menggantikan pernyataan
return. Defer tidak menentukan nilai kembalian fungsi kecuali fungsi itu memakai hasil bernama yang dibaca lewat closure. Perbaikannya, perlakukan defer sebagai pembersih alur, sedangkan nilai kembalian tetap ditentukan oleh pernyataanreturn.
Ringkasan#
defermenjadwalkan pemanggilan fungsi hingga fungsi pengelilingnya selesai.- Urutan eksekusinya LIFO, yaitu yang ditunda terakhir dijalankan lebih dahulu.
- Argumen dievaluasi pada saat pernyataan defer ditulis, sedangkan closure membaca nilai terbaru variabelnya.
- Fungsi tertunda dapat membaca dan menyunting hasil bernama, dan idiom utamanya adalah menutup sumber daya tepat setelah sumber daya itu berhasil diperoleh.
Referensi#
- Defer, Panic, and Recover — The Go Blog — Mekanisme defer beserta contoh idiom pembersihannya.
- The Go Programming Language Specification — The Go Authors — Aturan eksekusi pernyataan defer menurut spesifikasi bahasa.