Menu

Defer di Golang: Urutan, Evaluasi Argumen, dan Jebakannya

defer menjadwalkan sebuah pemanggilan untuk dijalankan saat fungsi di sekitarnya selesai. Pelajari urutan LIFO, kapan argumen dievaluasi, menutup file dan membuka kunci mutex, defer di dalam loop, dan mengubah hasil bernama.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Apa yang dilakukan defer

defer memasukkan sebuah pemanggilan fungsi ke dalam daftar. Saat fungsi di sekitarnya selesai, daftar itu dijalankan dengan urutan terbalik.

Output:

start
end
deferred 3
deferred 2
deferred 1

Urutannya adalah terakhir masuk, pertama keluar, seperti stack. Itu sesuai dengan cara resource bersarang: kalau kamu membuka A lalu B, biasanya kamu ingin menutup B sebelum A.

Pembersihan di sebelah pengambilan resource

Kegunaan utama defer adalah menulis kode pembersihan tepat di baris setelah resource diambil, sehingga tidak ada jalur return yang bisa melupakannya.

Perhatikan urutannya: periksa error dulu, baru defer. Kalau os.Open gagal, f bernilai nil, dan men-defer f.Close() sebelum pemeriksaan akan memanggil Close pada *os.File yang nil (yang mengembalikan error yang tidak pernah kamu lihat, dan membingungkan pembaca kode).

Bentuk yang sama berlaku untuk lock:

mu.Lock()
defer mu.Unlock()

Kalau kode di antara keduanya panic, kunci mutex tetap dibuka.

Argumen langsung dievaluasi

Fungsi yang di-defer beserta argumennya dievaluasi saat pernyataan defer dijalankan. Hanya pemanggilannya yang menunggu.

Output:

x is now 2
deferred closure reads: 2
deferred with argument: 1

fmt.Println("...", x) menangkap nilai 1 di baris defer. Closure tidak punya argumen; ia membaca x saat akhirnya dijalankan. Pilih bentuk yang sesuai dengan apa yang ingin kamu catat.

Aturan ini juga berlaku untuk receiver method. defer t.Stop() langsung mengevaluasi t, jadi meng-assign ulang t setelahnya tidak mengubah nilai mana yang dihentikan.

Trik pengukuran waktu yang umum memakai aturan ini dengan sengaja:

func handle() {
	defer trace("handle")() // trace runs now, the returned func runs at exit
	// ...
}

trace("handle") langsung dipanggil (ia bisa mencetak "enter" dan mencatat waktu mulai), dan fungsi yang dikembalikannya itulah yang di-defer.

Defer di dalam loop

Pemanggilan defer berjalan saat fungsi selesai, bukan di akhir setiap iterasi loop. Di dalam loop yang memproses banyak file, ini membuat setiap file tetap terbuka sampai fungsinya selesai.

for _, path := range paths {
	f, err := os.Open(path)
	if err != nil {
		return err
	}
	defer f.Close() // all files stay open until the function returns
	process(f)
}

Dengan ribuan path, ini akan kehabisan file descriptor. Pindahkan badan loop ke fungsinya sendiri supaya setiap defer berjalan per iterasi:

Setiap pemanggilan processFile menutup file-nya sebelum file berikutnya dibuka. Function literal yang langsung dipanggil di tempat (func() { ... }()) bekerja dengan cara yang sama saat fungsi helper bernama terasa berlebihan.

Mengubah nilai kembalian

Closure yang di-defer berjalan setelah pernyataan return mengisi hasilnya, dan closure itu bisa mengubah hasil bernama sebelum pemanggil melihatnya.

Program ini mencetak 10 dan save failed: disk full. Dengan hasil yang tidak bernama, fungsi yang di-defer tetap bisa berjalan, tetapi tidak punya cara untuk mengubah apa yang dikembalikan.

Menangkap error dari Close

defer f.Close() membuang error dari Close. Untuk file yang hanya kamu baca, itu tidak masalah. Untuk file yang kamu tulis, Close bisa melaporkan flush terakhir yang gagal, jadi error-nya penting. Hasil bernama memungkinkanmu menyimpannya:

func writeReport(path string, data []byte) (err error) {
	f, err := os.Create(path)
	if err != nil {
		return err
	}
	defer func() {
		if cerr := f.Close(); cerr != nil && err == nil {
			err = cerr
		}
	}()
	_, err = f.Write(data)
	return err
}

errors.Join(err, f.Close()) adalah alternatif yang lebih singkat saat kamu ingin kedua error dilaporkan.

Defer, panic, dan recover

Pemanggilan defer berjalan selama panic membongkar stack. Hanya di sanalah recover berpengaruh, dan begitulah cara sebuah server mencegah satu request yang bermasalah mematikan seluruh proses. Detailnya ada di panic dan recover.

Pemanggilan defer tidak berjalan saat program keluar lewat os.Exit atau log.Fatal. Kalau main men-defer pembersihan lalu memanggil os.Exit(1), pembersihan itu dilewati.

Biaya

Sejak Go 1.14, kebanyakan defer di-open-code oleh compiler dan hanya memakan beberapa nanodetik. Memakai defer untuk setiap pelepasan mutex dan penutupan file adalah gaya yang normal. Defer di dalam loop adalah pengecualian: defer seperti itu tidak bisa di-open-code dan memakai jalur yang lebih lambat, satu alasan lagi untuk memindahkan badan loop ke dalam fungsi.

Kesalahan umum

  • Men-defer sebelum memeriksa error. Periksa err dari Open dulu, lalu defer Close.
  • Mengharapkan pembersihan per iterasi di dalam loop. Defer berjalan saat fungsi selesai.
  • Mengharapkan argumen yang di-defer melihat perubahan sesudahnya. Argumen sudah tetap di baris defer. Pakai closure untuk membaca nilainya saat fungsi selesai.
  • Mengandalkan defer bersama os.Exit. Defer tidak pernah berjalan.

Pertanyaan yang sering diajukan

Apa fungsi defer di Go?

defer f() menjadwalkan f() untuk dijalankan saat fungsi di sekitarnya selesai, entah selesai secara normal, lewat return yang lebih awal, atau karena panic. Defer dipakai untuk menaruh kode pembersihan (menutup file, membuka kunci mutex) tepat di sebelah kode yang mengambil resource tersebut.

Dalam urutan apa pemanggilan defer dijalankan di Go?

Terakhir masuk, pertama keluar (LIFO). Pemanggilan yang paling akhir di-defer berjalan paling dulu. defer fmt.Println(1); defer fmt.Println(2) mencetak 2 lalu 1.

Kapan argumen dari fungsi yang di-defer dievaluasi?

Langsung, saat pernyataan defer dieksekusi, bukan saat pemanggilannya berjalan. x := 1; defer fmt.Println(x); x = 2 mencetak 1. Untuk membaca nilainya saat fungsi selesai, defer sebuah closure: defer func() { fmt.Println(x) }().

Apakah defer tetap berjalan saat panic atau os.Exit?

Pemanggilan defer berjalan selama panic membongkar stack, dan karena itulah recover berfungsi di dalamnya. Defer tidak berjalan saat program memanggil os.Exit (atau log.Fatal, yang memanggilnya), dan tidak berjalan di goroutine lain saat main selesai.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai