Setiap nilai dalam program Go perlu sebuah rumah di memori, dan compiler yang memutuskan alamatnya: stack milik fungsi pencipta, atau heap yang dikelola garbage collector. Keputusan tersebut diambil oleh sebuah analisis pada waktu kompilasi bernama escape analysis, yang memeriksa apakah sebuah nilai dapat “lolos” dari fungsi yang menciptakannya. Kabar baiknya, keputusan ini tidak tertutup rapat — compiler bersedia menjelaskan alasannya lewat flag -gcflags=-m. Chapter ini membaca keputusan tersebut langsung dari laporan compiler sungguhan.
Apa Itu Escape Analysis #
Saat mengompilasi sebuah fungsi, compiler menelusuri setiap variabel lokal dan bertanya: apakah referensi ke variabel ini masih hidup setelah fungsi kembali? Ketika jawabannya tidak, variabel ditempatkan pada stack frame fungsi — memori yang lenyap otomatis saat fungsi selesai, tanpa biaya alokasi dan tanpa pekerjaan garbage collector. Ketika jawabannya ya, variabel dikatakan escape (lolos) dan compiler memindahkannya ke heap agar masa hidupnya melampaui fungsi penciptanya. Istilah yang sering muncul pada laporan compiler adalah moved to heap untuk pemindahan variabel dan escapes to heap untuk nilai yang lolos.
Kapan Sebuah Nilai Lolos ke Heap #
Beberapa pola hampir selalu memaksa nilai naik ke heap:
- alamat variabel lokal dikembalikan sebagai nilai kembalian pointer;
- alamat variabel lokal disimpan ke variabel global atau ke objek lain yang sudah berada di heap;
- variabel ditangkap oleh closure yang dijalankan sebagai goroutine — laporan compiler mencatatnya dengan baris
func literal escapes to heap; - pointer ke variabel dikirim lewat channel sehingga umurnya tidak lagi dikendalikan fungsi pencipta;
- nilai dikonversi ke interface, termasuk saat dikirim sebagai argumen fungsi variadic seperti
fmt.Println.
Contoh Utuh: Dua Variabel yang Nasibnya Berbeda #
Program berikut memuat tiga fungsi kecil dengan pola lolos yang berbeda. Fungsi buatDiHeap mengembalikan alamat variabel lokal, fungsi buatDiStack hanya mengembalikan nilai, dan fungsi simpanGlobal menyimpan alamat variabel lokal ke variabel global:
package main
import "fmt"
var simpan *int
// buatDiHeap mengembalikan alamat variabel lokal.
// Alamat x keluar dari fungsi sehingga x dipindahkan ke heap.
func buatDiHeap() *int {
x := 42
return &x
}
// buatDiStack hanya mengembalikan nilai.
// Nilai disalin ke pemanggil sehingga x tetap tinggal di stack.
func buatDiStack() int {
x := 42
return x
}
// simpanGlobal menyimpan alamat v ke variabel global.
// v ditunjuk dari luar fungsi sehingga v juga dipindah ke heap.
func simpanGlobal() {
v := 99
simpan = &v
}
func main() {
p := buatDiHeap()
simpanGlobal()
total := *p + *simpan + buatDiStack()
fmt.Println("total:", total)
}Jalankan program tersebut dari direktori chapter dengan perintah berikut:
go run escape-demo.goOutput yang Dihasilkan:
total: 183Mari kita telaah baris demi baris program di atas:
- Baris 5 (
var simpan *int): variabel global ini menjadi penampung alamat; siapa pun yang alamatnya tersimpan di sini harus bertahan selama program berjalan. - Baris 10-11 (
return &x): variabelxdiisi42, lalu alamatnya dikembalikan daribuatDiHeap— keluarnya alamat ini membuat compiler memindahkanxke heap, sehingga nilainya tetap valid setelah fungsi selesai. - Baris 17-18 (
return x):buatDiStackhanya mengembalikan salinan nilai; variabelxtidak perlu dipindah dan tetap tinggal di stack. - Baris 24-25 (
simpan = &v): alamatvdisimpan ke variabel global, sehinggavjuga dipindahkan ke heap. - Baris 31 (
total := ...): penjumlahan membaca nilai-nilai tersebut tanpa menyimpan alamatnya, lalu baris 32 mencetak hasil183.
Membaca Laporan Compiler dengan -gcflags=-m #
Perintah go build menerima flag -gcflags yang diteruskan ke compiler. Flag -m meminta compiler mencetak alasan keputusan escape analysis. Tambahkan -l untuk menonaktifkan inlining agar laporan fokus pada keputusan memori saja:
go build -gcflags="-m -l" escape-demo.goOutput yang Dihasilkan:
# command-line-arguments
./escape-demo.go:10:2: moved to heap: x
./escape-demo.go:24:2: moved to heap: v
./escape-demo.go:32:13: ... argument does not escape
./escape-demo.go:32:14: "total:" escapes to heap
./escape-demo.go:32:24: total escapes to heapLima baris tersebut menceritakan seluruh keputusan memori program kita. Baris moved to heap: x pada baris 10 dan moved to heap: v pada baris 24 sesuai dengan dua pola lolos yang kita bahas. Perhatikan pula yang tidak muncul: tidak ada satu baris pun tentang variabel x di dalam buatDiStack, karena variabel tersebut tidak meloloskan dirinya — compiler menempatkannya di stack secara diam-diam. Tiga baris terakhir menjelaskan efek fmt.Println pada baris 32: argumen "total:" dan total dikonversi ke interface, dan nilai-nilai tersebut dialokasikan di heap agar fungsi cetak dapat membacanya.
Referensi #
- FAQ: How do I know whether a variable is allocated on the heap or the stack? — jawaban resmi tentang escape analysis.
- Diagnostics — daftar alat diagnosis Go, termasuk flag compiler.
Escape analysis memberi kita peta alasan; peta tersebut akan sangat berarti setelah kita mengenal dua wilayah yang diperebutkan: stack yang ringan dan heap yang dikelola garbage collector. Kedua area memori tersebut kita bedah pada chapter berikutnya.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
escape-demo.go
Unduh berkaspackage main
import "fmt"
var simpan *int
// buatDiHeap mengembalikan alamat variabel lokal.
// Alamat x keluar dari fungsi sehingga x dipindahkan ke heap.
func buatDiHeap() *int {
x := 42
return &x
}
// buatDiStack hanya mengembalikan nilai.
// Nilai disalin ke pemanggil sehingga x tetap tinggal di stack.
func buatDiStack() int {
x := 42
return x
}
// simpanGlobal menyimpan alamat v ke variabel global.
// v ditunjuk dari luar fungsi sehingga v juga dipindah ke heap.
func simpanGlobal() {
v := 99
simpan = &v
}
func main() {
p := buatDiHeap()
simpanGlobal()
total := *p + *simpan + buatDiStack()
fmt.Println("total:", total)
}