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:

go
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:

bash
go run escape-demo.go

Output yang Dihasilkan:

text
total: 183

Mari kita telaah baris demi baris program di atas:

  1. Baris 5 (var simpan *int): variabel global ini menjadi penampung alamat; siapa pun yang alamatnya tersimpan di sini harus bertahan selama program berjalan.
  2. Baris 10-11 (return &x): variabel x diisi 42, lalu alamatnya dikembalikan dari buatDiHeap — keluarnya alamat ini membuat compiler memindahkan x ke heap, sehingga nilainya tetap valid setelah fungsi selesai.
  3. Baris 17-18 (return x): buatDiStack hanya mengembalikan salinan nilai; variabel x tidak perlu dipindah dan tetap tinggal di stack.
  4. Baris 24-25 (simpan = &v): alamat v disimpan ke variabel global, sehingga v juga dipindahkan ke heap.
  5. Baris 31 (total := ...): penjumlahan membaca nilai-nilai tersebut tanpa menyimpan alamatnya, lalu baris 32 mencetak hasil 183.

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:

bash
go build -gcflags="-m -l" escape-demo.go

Output yang Dihasilkan:

text
# 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 heap

Lima 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 #

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 berkas
go
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)
}