Program Go menyimpan datanya di dua area memori dengan karakter yang bertolak belakang. Stack adalah memori kerja per fungsi yang hidupnya pendek dan pengembaliannya gratis, sedangkan heap adalah gudang besar yang dikelola garbage collector untuk data yang harus bertahan lama. Kedua area tersebut tidak saling menggantikan — keduanya bekerja sama, dan keseimbangan pemakaiannya menentukan performa program. Chapter ini mengenali keduanya, membuktikan bahwa stack goroutine sangat murah, lalu membongkar mitos bahwa setiap make selalu menghabiskan heap.

Stack: Memori Goroutine yang Ringan #

Setiap goroutine memiliki stack sendiri yang dimulai dalam ukuran kecil — hanya beberapa kilobyte. Ketika sebuah fungsi dipanggil, runtime menyisipkan satu frame berisi variabel lokal di ujung stack; ketika fungsi kembali, frame tersebut lenyap begitu saja tanpa kerja apa pun. Tidak ada pencatatan, tidak ada pembongkaran, dan tidak ada garbage collector yang ikut campur. Ketika frame berikutnya tidak muat, runtime mengalokasikan blok stack yang lebih besar, menyalin isi lama ke blok baru, lalu melanjutkan eksekusi — proses tumbuh yang transparan bagi kode kita. Biaya stack karenanya sangat rendah: memanggil fungsi jutaan kali pun tidak menambah satu butir pun pekerjaan garbage collector.

Heap: Area Global Milik Garbage Collector #

Heap menyimpan data yang masa hidupnya melampaui fungsi penciptanya: hasil buatDiHeap dari chapter sebelumnya, map besar yang dipakai selama program berjalan, atau buffer yang dikirim antar goroutine. Berbeda dengan stack, heap tidak dibongkar secara otomatis saat fungsi selesai; pembebasannya menjadi tugas garbage collector yang kita bahas pada chapter berikutnya. Setiap alokasi heap membawa dua biaya: waktu pemesanan memori saat alokasi terjadi, dan pekerjaan penandaan ketika GC menyapu heap. Program dengan alokasi heap yang rendah umumnya lebih cepat dan lebih hemat memori — tema yang kita dalami pada section Performance dan Optimisasi .

Contoh Utuh: Seribu Goroutine Tetap Murah #

Program berikut memulai 1000 goroutine yang sama-sama menunggu satu channel. Ketika seluruh goroutine berjalan, program mencatat jumlahnya lewat runtime.NumGoroutine, lalu menutup channel untuk membebaskan semuanya:

go
package main

import (
	"fmt"
	"runtime"
	"sync"
	"sync/atomic"
)

func main() {
	fmt.Println("goroutine di awal    :", runtime.NumGoroutine())

	const jumlah = 1000
	var siap atomic.Int32
	var wg sync.WaitGroup
	mulai := make(chan struct{})

	wg.Add(jumlah)
	for range jumlah {
		go func() {
			defer wg.Done()
			siap.Add(1)
			<-mulai // semua goroutine menunggu sinyal mulai
		}()
	}

	// tunggu sampai 1000 goroutine benar-benar tercipta
	for siap.Load() < jumlah {
		runtime.Gosched()
	}

	fmt.Println("goroutine saat sibuk :", runtime.NumGoroutine())
	close(mulai) // satu channel tutup membebaskan 1000 goroutine
	wg.Wait()
	fmt.Println("goroutine di akhir   :", runtime.NumGoroutine())
}

Jalankan program tersebut dari direktori chapter dengan perintah berikut:

bash
go run stack-heap-demo.go

Output yang Dihasilkan:

text
goroutine di awal    : 1
goroutine saat sibuk : 1001
goroutine di akhir   : 1

Mari kita telaah baris demi baris program di atas:

  1. Baris 11 (runtime.NumGoroutine()): fungsi dari package runtime ini menghitung goroutine yang sedang ada; di awal program hanya ada goroutine main sehingga hasilnya 1.
  2. Baris 16 (mulai := make(chan struct{})): channel ini menjadi palang tunggu bersama; elemen bertipe struct{} berukuran nol byte sehingga tidak ada data yang berpindah.
  3. Baris 20-25 (go func()): setiap goroutine menandai dirinya siap melalui siap.Add(1) lalu menunggu channel; 1000 stack kecil tercipta dengan biaya yang sangat rendah.
  4. Baris 28-30 (for siap.Load() < jumlah): loop ini menunggu seluruh goroutine lahir; runtime.Gosched() menyerahkan jadwal CPU agar goroutine baru sempat menandai kesiapannya.
  5. Baris 32 ("goroutine saat sibuk :"): saat seluruh goroutine menunggu, NumGoroutine mencatat 1001 — goroutine main ditambah 1000 pekerja.
  6. Baris 33-35 (close(mulai)): penutupan channel membebaskan semua penerima sekaligus; setelah wg.Wait() selesai, hitungan kembali ke 1.

Angka 1001 pada output membuktikan klaim utama bab ini: stack goroutine begitu ringan sehingga ribuan goroutine aktif bukanlah beban memori yang berarti. Beban baru muncul ketika goroutine-goroutine tersebut menyimpan data besar di heap.

Mitos “make Selalu Menghabiskan Heap” #

Banyak pembelajar menyimpulkan bahwa make selalu mengalokasikan memori di heap. Kesimpulan tersebut keliru: selama hasil make tidak lolos dari fungsinya, compiler boleh menempatkannya di stack. Simpan program kecil berikut sebagai cek-alloc.go, lalu minta compiler menjelaskan keputusannya:

go
package main

func main() {
	m := make(map[int]int)
	m[1] = 1
	_ = m

	s := make([]int, 4)
	s[0] = 5
	_ = s

	b := make([]byte, 64)
	_ = b
}
bash
go build -gcflags="-m -l" cek-alloc.go

Output yang Dihasilkan:

text
# command-line-arguments
./cek-alloc.go:4:11: make(map[int]int) does not escape
./cek-alloc.go:8:11: make([]int, 4) does not escape
./cek-alloc.go:12:11: make([]byte, 64) does not escape

Kata kunci pada ketiga baris tersebut adalah does not escape: map, slice, dan buffer kecil yang tidak meninggalkan fungsi penciptanya tetap ditangani di stack. Keputusan tersebut berbalik begitu nilainya lolos — map yang dikembalikan sebagai pointer atau slice yang dikirim ke channel akan naik ke heap dan menanggung biaya GC.

Referensi #

Heap yang terus terisi oleh alokasi akhirnya harus dibersihkan oleh seseorang. Chapter berikutnya memperkenalkan pembersih otomatis tersebut: garbage collector, lengkap dengan cara mengatur semangat kerjanya.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

stack-heap-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"runtime"
	"sync"
	"sync/atomic"
)

func main() {
	fmt.Println("goroutine di awal    :", runtime.NumGoroutine())

	const jumlah = 1000
	var siap atomic.Int32
	var wg sync.WaitGroup
	mulai := make(chan struct{})

	wg.Add(jumlah)
	for range jumlah {
		go func() {
			defer wg.Done()
			siap.Add(1)
			<-mulai // semua goroutine menunggu sinyal mulai
		}()
	}

	// tunggu sampai 1000 goroutine benar-benar tercipta
	for siap.Load() < jumlah {
		runtime.Gosched()
	}

	fmt.Println("goroutine saat sibuk :", runtime.NumGoroutine())
	close(mulai) // satu channel tutup membebaskan 1000 goroutine
	wg.Wait()
	fmt.Println("goroutine di akhir   :", runtime.NumGoroutine())
}