Goroutine murah, tetapi goroutine yang tidak pernah berakhir tetap menumpuk: tumpukan memori, referensi data, dan slot penjadwalan yang tidak pernah kembali — kebocoran memori yang sumbernya bukan alokasi, melainkan alur eksekusi tanpa jalan keluar. Server yang berjalan berbulan-bulan bisa membengkak hanya karena satu channel yang lupa ditutup. Chapter penutup ini mempelajari goroutine terkunci, membuktikan kebocorannya dengan runtime.NumGoroutine, lalu menutupnya dengan channel done dan context.

Goroutine yang Menggantung #

Dua bentuk paling umum: goroutine menunggu menerima dari channel yang tidak pernah dikirim siapa pun, dan goroutine mencoba mengirim ke channel yang tidak pernah dibaca — keduanya terkunci selamanya tanpa pesan galat. Program kecil tampak luput karena main yang kembali menghentikan seluruh proses; pada server jangka panjang, main tidak pernah kembali, dan goroutine bocor menumpuk diam-diam.

Membuktikan Leak dengan runtime.NumGoroutine #

Fungsi runtime.NumGoroutine() melaporkan jumlah goroutine yang hidup. Berkas leak-demo.go memotret angka itu sebelum dan sesudah memulai tiga goroutine yang menunggu channel pesan — sengaja tidak pernah ditutup:

go
package main

import (
	"fmt"
	"runtime"
)

func main() {
	pesan := make(chan string) // channel ini sengaja tidak pernah ditutup
	sinyal := make(chan struct{})

	fmt.Println("goroutine awal  :", runtime.NumGoroutine())

	for range 3 {
		go func() {
			sinyal <- struct{}{} // goroutine sudah berjalan
			<-pesan              // menunggu selamanya: goroutine leak
		}()
	}
	for range 3 {
		<-sinyal
	}

	fmt.Println("goroutine bocor :", runtime.NumGoroutine())
}

Jalankan program tersebut dari direktori chapter:

bash
go run leak-demo.go

Output yang Dihasilkan:

text
goroutine awal  : 1
goroutine bocor : 4

Mari kita telaah baris demi baris program di atas:

  1. Baris 9–10 (dua channel): pesan tanpa pengirim dan tanpa penutup; sinyal memastikan goroutine sudah hidup.
  2. Baris 12: foto pertama — hanya main yang hidup, angka 1.
  3. Baris 14–19 (goroutine bocor): setiap goroutine mengirim tanda ke sinyal lalu berdiri menghadap <-pesan selamanya.
  4. Baris 20–22 (<-sinyal tiga kali): main menunggu ketiga tanda — sinkronisasi eksplisit agar penghitungan tidak mendahului hidupnya goroutine.
  5. Baris 24: foto kedua — main plus tiga goroutine terkunci: angka 4.

Angka 4 stabil di setiap eksekusi: pernyataan go terhitung seketika dan ketiganya tidak mungkin keluar. Program tetap berakhir kode 0 — main kembali, runtime melenyapkan sisanya — tetapi di server, tiga goroutine itu akan menunggu selama hidup proses.

Solusi: Channel done dan select #

Perbaikannya memberi goroutine jalur keluar: channel done yang ditutup pemilik pekerjaan saat hasil tak lagi dibutuhkan. Penerimaan dari channel tertutup selalu berhasil segera, sehingga satu penutupan melepaskan semua goroutine yang menunggu sekaligus. Berkas leak-fixed-demo.go menerapkannya:

go
package main

import (
	"fmt"
	"runtime"
	"sync"
	"time"
)

func main() {
	pesan := make(chan string) // tidak ada pengirim pada contoh ini
	done := make(chan struct{})
	var wg sync.WaitGroup

	fmt.Println("goroutine awal  :", runtime.NumGoroutine())

	for range 3 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			select {
			case p := <-pesan:
				fmt.Println("menerima pesan:", p)
			case <-done:
				// sinyal berhenti: goroutine keluar dengan rapi
			}
		}()
	}

	close(done)
	wg.Wait()
	time.Sleep(20 * time.Millisecond) // beri runtime waktu memusnahkan goroutine

	fmt.Println("goroutine akhir :", runtime.NumGoroutine())
}

Jalankan program tersebut dari direktori chapter:

bash
go run leak-fixed-demo.go

Output yang Dihasilkan:

text
goroutine awal  : 1
goroutine akhir : 1

Mari kita telaah baris demi baris program di atas:

  1. Baris 11–13: pesan tetap tanpa pengirim, tetapi kini ada done sebagai jalur keluar dan wg untuk menunggu goroutine selesai.
  2. Baris 17–28 (goroutine pekerja): select memperebutkan pesan yang mungkin datang dengan done yang ditutup; cabang <-done mengembalikan kontrol sehingga goroutine keluar dengan rapi.
  3. Baris 30 (close(done)): satu penutupan melepaskan ketiga goroutine sekaligus — penutupan channel adalah siaran, bukan pesan satu-lawan-satu.
  4. Baris 31 (wg.Wait()): memastikan fungsi pekerja selesai sebelum penghitungan akhir.
  5. Baris 32 (time.Sleep): jeda 20 milidetik memberi runtime waktu memusnahkan struct goroutine — wg.Wait hanya menjamin fungsinya selesai, penghapusan goroutine berjalan asinkron; tanpa jeda, angka bisa sesaat menyisakan goroutine yang sedang dibongkar.
  6. Baris 34: foto akhir kembali ke 1 — tidak ada yang bocor.

Alternatif: context #

Untuk kode yang sudah menerima context.Context, pola yang sama tersedia lewat ctx.Done() — khususnya bila pembatalan perlu menjalar ke banyak bagian program:

go
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go func() {
	select {
	case p := <-pesan:
		fmt.Println("menerima:", p)
	case <-ctx.Done():
		// keluar saat cancel() dipanggil atau tenggat habis
	}
}()

Satu cancel() menutup Done() context beserta seluruh turunannya — dijelaskan tuntas di chapter context dan Cancellation dan Timeout .

Kebiasaan Pencegahan #

Empat kebiasaan menutup sebagian besar celah: beri setiap goroutine kondisi berhenti eksplisit; jadikan penutupan channel tanggung jawab pengirim; alirkan context sebagai parameter pertama; dan dalam test, bandingkan runtime.NumGoroutine() sebelum-sesudah — atau pakai pustaka goleak — agar bocor tertangkap sebelum produksi.

Peta concurrency kita kini lengkap: goroutine dan channel sebagai fondasi, select dan package sync sebagai koordinasi, context sebagai rem darurat, worker pool dan pipeline sebagai pola susunan, serta disiplin race dan leak sebagai penjaga kualitas. Bekal itu terpakai penuh di proyek Concurrent Worker Pool — dan sebelum sampai di sana, section berikutnya mengurai isi Standard Library Go.

Referensi #

Berkas Sumber

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

leak-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"runtime"
)

func main() {
	pesan := make(chan string) // channel ini sengaja tidak pernah ditutup
	sinyal := make(chan struct{})

	fmt.Println("goroutine awal  :", runtime.NumGoroutine())

	for range 3 {
		go func() {
			sinyal <- struct{}{} // goroutine sudah berjalan
			<-pesan              // menunggu selamanya: goroutine leak
		}()
	}
	for range 3 {
		<-sinyal
	}

	fmt.Println("goroutine bocor :", runtime.NumGoroutine())
}

leak-fixed-demo.go

Unduh berkas
go
package main

import (
	"fmt"
	"runtime"
	"sync"
	"time"
)

func main() {
	pesan := make(chan string) // tidak ada pengirim pada contoh ini
	done := make(chan struct{})
	var wg sync.WaitGroup

	fmt.Println("goroutine awal  :", runtime.NumGoroutine())

	for range 3 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			select {
			case p := <-pesan:
				fmt.Println("menerima pesan:", p)
			case <-done:
				// sinyal berhenti: goroutine keluar dengan rapi
			}
		}()
	}

	close(done)
	wg.Wait()
	time.Sleep(20 * time.Millisecond) // beri runtime waktu memusnahkan goroutine

	fmt.Println("goroutine akhir :", runtime.NumGoroutine())
}