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:
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:
go run stack-heap-demo.goOutput yang Dihasilkan:
goroutine di awal : 1
goroutine saat sibuk : 1001
goroutine di akhir : 1Mari kita telaah baris demi baris program di atas:
- Baris 11 (
runtime.NumGoroutine()): fungsi dari packageruntimeini menghitung goroutine yang sedang ada; di awal program hanya ada goroutinemainsehingga hasilnya1. - Baris 16 (
mulai := make(chan struct{})): channel ini menjadi palang tunggu bersama; elemen bertipestruct{}berukuran nol byte sehingga tidak ada data yang berpindah. - Baris 20-25 (
go func()): setiap goroutine menandai dirinya siap melaluisiap.Add(1)lalu menunggu channel; 1000 stack kecil tercipta dengan biaya yang sangat rendah. - Baris 28-30 (
for siap.Load() < jumlah): loop ini menunggu seluruh goroutine lahir;runtime.Gosched()menyerahkan jadwal CPU agar goroutine baru sempat menandai kesiapannya. - Baris 32 (
"goroutine saat sibuk :"): saat seluruh goroutine menunggu,NumGoroutinemencatat1001— goroutinemainditambah 1000 pekerja. - Baris 33-35 (
close(mulai)): penutupan channel membebaskan semua penerima sekaligus; setelahwg.Wait()selesai, hitungan kembali ke1.
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:
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
}go build -gcflags="-m -l" cek-alloc.goOutput yang Dihasilkan:
# 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 escapeKata 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 #
- FAQ: How do I know whether a variable is allocated on the heap or the stack? — penjelasan resmi pembagian stack dan heap.
- Package runtime
— fungsi introspeksi runtime termasuk
NumGoroutine.
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 berkaspackage 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())
}