Garbage collector membebaskan memori yang tidak lagi dirujuk, dan justru di situ letak masalahnya: dalam Go, kebocoran memori hampir selalu terjadi karena data masih dirujuk oleh sesuatu yang lupa kita bebaskan, bukan karena GC gagal bekerja. Kebocoran jarang meledak seketika — memori program naik perlahan setiap jam, lalu layanan tumbang di tengah malam. Chapter penutup section ini merangkum pola referensi tersembunyi yang paling sering menjadi biang keroknya, membuktikan dua pola besar dengan angka yang bisa diverifikasi, dan menunjukkan jalur deteksinya lewat profiling.
Pola Kebocoran yang Sering Muncul #
Subslice Menahan Backing Array #
Mengiris sepuluh elemen dari slice berisi satu juta elemen terasa seperti membuang sisanya, padahal slice kecil tersebut masih memegang pointer ke backing array penuh. Selama subslice hidup, GC tidak dapat membebaskan alokasi aslinya. Perbaikannya: salin data yang dibutuhkan ke slice baru dengan copy, sehingga array besar benar-benar terlepas — bukti angkanya kita lihat pada contoh utuh di bawah.
Goroutine yang Tak Pernah Pulang #
Goroutine yang menunggu channel yang tidak pernah dikirim atau ditutup akan menunggu selamanya, dan seluruh data yang ditangkap closure-nya ikut terjaga. Pola ini sering muncul pada worker yang dibuat per request tetapi tidak diberi jalur keluar. Perbaikannya: beri setiap goroutine channel penutupan atau context pembatal, lalu pastikan jalur tersebut benar-benar dipanggil sebelum pemanggil selesai.
Map Global yang Tumbuh Tanpa Batas #
var cache = make(map[string][]byte)
func simpan(kunci string, data []byte) {
cache[kunci] = data // setiap kunci baru menambah isi map selamanya
}Map di variabel global hidup seumur program, sehingga cache tanpa kebijakan penghapusan tumbuh tanpa hingga. Perbaikannya: tetapkan batas jumlah entri, hapus entri tua secara berkala, atau simpan data besar di penyimpanan eksternal dan biarkan map hanya menyimpan indeks.
Ticker Tanpa Stop #
func pantau() {
t := time.NewTicker(time.Minute)
defer t.Stop() // tanpa Stop, ticker terus berdetak seumur program
for range t.C {
// ... memproses sesuatu secara periodik ...
}
}Ticker yang dibuat lewat time.NewTicker tetap berdetak sampai Stop dipanggil. Ketika sebuah fungsi pembuat ticker kembali tanpa Stop, runtime timer dan channel-nya tetap hidup tanpa manfaat. Biasakan defer t.Stop() berdampingan dengan pembuatnya.
defer di Dalam Loop Panjang #
func prosesSemua(berkas []string) error {
for _, nama := range berkas {
f, err := os.Open(nama)
if err != nil {
return err
}
defer f.Close() // seluruh Close menumpuk sampai fungsi selesai
// ... memproses isi f ...
}
return nil
}Pernyataan defer baru dieksekusi ketika fungsi pembungkusnya kembali. Di dalam loop panjang, ratusan defer menumpuk dan ratusan berkas tetap terbuka sampai loop selesai. Perbaikannya: pindahkan isi loop menjadi fungsi tersendiri sehingga setiap defer dieksekusi per iterasi.
Contoh Utuh: Bukti dengan Angka #
Program berikut membuktikan dua pola secara terukur: kelima pekerja goroutine benar-benar pergi setelah sinyal penutupan dikirim, dan subslice menahan backing array penuh yang tak terlihat dari len-nya:
package main
import (
"fmt"
"runtime"
"sync/atomic"
"time"
)
// tahanBackingArray mengembalikan potongan kecil dari array besar.
// variabel kecil masih menahan backing array berukuran satu juta elemen,
// sedangkan salinan benar-benar berdiri sendiri.
func tahanBackingArray() (kecil, salinan []int) {
besar := make([]int, 1_000_000) // ±8 MiB
for i := range besar {
besar[i] = i
}
kecil = besar[:10]
salinan = make([]int, 10)
copy(salinan, kecil)
return kecil, salinan
}
func main() {
fmt.Println("goroutine awal :", runtime.NumGoroutine())
// pola pekerja: goroutine menunggu sinyal penutupan channel.
// jika channel tidak pernah ditutup, goroutine menunggu selamanya.
const jumlah = 5
var siap atomic.Int32
sinyal := make([]chan struct{}, jumlah)
for i := range sinyal {
sinyal[i] = make(chan struct{})
go func() {
siap.Add(1)
<-sinyal[i]
}()
}
// tunggu sampai seluruh pekerja tercipta dan menunggu
for siap.Load() < jumlah {
time.Sleep(time.Millisecond)
}
fmt.Println("pekerja menunggu:", runtime.NumGoroutine())
// cleanup: tutup setiap channel sehingga pekerja boleh selesai
for i := range sinyal {
close(sinyal[i])
}
for runtime.NumGoroutine() > 1 {
time.Sleep(time.Millisecond)
}
fmt.Println("setelah cleanup :", runtime.NumGoroutine())
kecil, salinan := tahanBackingArray()
fmt.Println("kecil -> len:", len(kecil), "cap:", cap(kecil))
fmt.Println("salinan -> len:", len(salinan), "cap:", cap(salinan))
}Jalankan program tersebut dari direktori chapter dengan perintah berikut:
go run memory-leak-demo.goOutput yang Dihasilkan:
goroutine awal : 1
pekerja menunggu: 6
setelah cleanup : 1
kecil -> len: 10 cap: 1000000
salinan -> len: 10 cap: 10Mari kita telaah baris demi baris program di atas:
- Baris 13-22 (
tahanBackingArray):kecil = besar[:10]membuat subslice;copy(salinan, kecil)membuat salinan mandiri yang melepaskan diri dari array besar. - Baris 29-38 (
go func()): lima goroutine pekerja masing-masing menunggu channel miliknya — kalau penutupan pada baris 47-49 dihapus, kelimanya menunggu selamanya dan hitungan tidak pernah kembali ke1. - Baris 41-44 (
siap.Load()): loop kecil menunggu seluruh pekerja tercipta, laluNumGoroutinemencatat6— goroutinemainditambah lima pekerja. - Baris 47-53 (
close(sinyal[i])): setelah setiap channel ditutup, jumlah goroutine kembali ke1; inilah alasan program ini sengaja melakukan cleanup, bukan membiarkan kebocoran sungguhan. - Baris 55-57 (
cap(kecil)): nilainya1000000— bukti bahwa backing array penuh masih ditahan meskilen-nya hanya10; salinan memilikicapsepuluh dan tidak menahan apa pun di luar itu.
Mendeteksi dengan pprof #
Ketika kebocoran sudah terjadi di lapangan, dugaan tidak cukup — kita membutuhkan profil heap. Package runtime/pprof merekam alokasi ke berkas profil, dan perintah go tool pprof menampilkan fungsi-fungsi penghasil alokasi terbesar berikut ukurannya. Petunjuk praktis lengkap tersedia pada chapter Memory Profiling
dan pprof
. Untuk kebocoran goroutine, angka runtime.NumGoroutine yang terus naik pada metrik layanan adalah alarm paling murah sebelum profil diambil.
Referensi #
- Diagnostics — resmi: profil memori dan runtime profiling.
- Package runtime: NumGoroutine — pemantau jumlah goroutine.
- Go Blog: Profiling Go programs — pengantar pprof dari tim Go.
Section Pointer dan Memory kita tutup di sini: pointer memberi kontrol atas alamat dan masa hidup data, escape analysis menentukan rumahnya, stack dan heap menjadi ladangnya, GC menjaganya, dan disiplin referensi menjaga kebersihannya. Kemampuan ini menjadi bekal utama saat data mulai dibungkus struct — struktur data dan method-nya menyambut kita pada section berikutnya.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
memory-leak-demo.go
Unduh berkaspackage main
import (
"fmt"
"runtime"
"sync/atomic"
"time"
)
// tahanBackingArray mengembalikan potongan kecil dari array besar.
// variabel kecil masih menahan backing array berukuran satu juta elemen,
// sedangkan salinan benar-benar berdiri sendiri.
func tahanBackingArray() (kecil, salinan []int) {
besar := make([]int, 1_000_000) // ±8 MiB
for i := range besar {
besar[i] = i
}
kecil = besar[:10]
salinan = make([]int, 10)
copy(salinan, kecil)
return kecil, salinan
}
func main() {
fmt.Println("goroutine awal :", runtime.NumGoroutine())
// pola pekerja: goroutine menunggu sinyal penutupan channel.
// jika channel tidak pernah ditutup, goroutine menunggu selamanya.
const jumlah = 5
var siap atomic.Int32
sinyal := make([]chan struct{}, jumlah)
for i := range sinyal {
sinyal[i] = make(chan struct{})
go func() {
siap.Add(1)
<-sinyal[i]
}()
}
// tunggu sampai seluruh pekerja tercipta dan menunggu
for siap.Load() < jumlah {
time.Sleep(time.Millisecond)
}
fmt.Println("pekerja menunggu:", runtime.NumGoroutine())
// cleanup: tutup setiap channel sehingga pekerja boleh selesai
for i := range sinyal {
close(sinyal[i])
}
for runtime.NumGoroutine() > 1 {
time.Sleep(time.Millisecond)
}
fmt.Println("setelah cleanup :", runtime.NumGoroutine())
kecil, salinan := tahanBackingArray()
fmt.Println("kecil -> len:", len(kecil), "cap:", cap(kecil))
fmt.Println("salinan -> len:", len(salinan), "cap:", cap(salinan))
}