โ† Semua pembelajaran / Go Nol โ†’ Enterprise
Fase 2 ยท Concurrency

Channel & select

"Jangan berkomunikasi dengan berbagi memori; berbagilah memori dengan berkomunikasi." Channel adalah bentuk konkret kalimat itu, dan sebagian besar bug channel berasal dari tiga aturan yang tidak diketahui.

Sumber asli go.dev Resmi Rangkuman ~9 menit baca

Intisari

  • Channel tanpa buffer menyinkronkan: pengirim menunggu sampai ada yang menerima.
  • Channel berbuffer menampung N nilai sebelum pengirim tertahan.
  • Hanya pengirim yang boleh menutup channel, dan hanya kalau pengirimnya satu. Menutup dua kali membuat panik.
  • Membaca dari channel tertutup langsung mengembalikan nilai nol; menulis ke channel tertutup membuat panik.
  • select memilih case yang siap; tambahkan <-ctx.Done() supaya tidak pernah menunggu selamanya.

Dua jenis channel

tanpaBuffer := make(chan int)      // pengirim menunggu penerima
berbuffer   := make(chan int, 10)  // pengirim jalan terus sampai 10 nilai menumpuk

berbuffer <- 1        // kirim
v := <-berbuffer      // terima
v, ok := <-berbuffer  // ok == false kalau channel sudah tertutup DAN kosong

close(berbuffer)      // hanya oleh pengirim
OperasiChannel nilChannel terbukaChannel tertutup
KirimMenggantung selamanyaSukses / menunggupanic
TerimaMenggantung selamanyaSukses / menungguNilai nol, ok=false
closepanicSuksespanic

Hafalkan baris "kirim ke channel tertutup = panic". Itu penyebab hampir semua crash yang melibatkan channel, dan ia selalu berarti hal yang sama: ada yang menutup channel dari sisi yang salah.

Siapa yang menutup

Aturannya: channel ditutup oleh pengirim, bukan penerima; dan hanya kalau pengirimnya satu. Penerima tidak perlu menutup apa pun โ€” channel yang tidak lagi dirujuk siapa pun akan dibersihkan garbage collector seperti nilai biasa.

Kalau ada banyak pengirim, jangan tutup dari sana. Pakai sync.WaitGroup untuk menunggu semua pengirim selesai, lalu tutup dari satu goroutine koordinator.

// Banyak pengirim, satu penutup
hasil := make(chan Hasil)
var wg sync.WaitGroup

for _, tugas := range daftar {
	wg.Add(1)
	go func() {
		defer wg.Done()
		hasil <- kerjakan(tugas)
	}()
}

go func() {
	wg.Wait()
	close(hasil)   // satu-satunya penutup, setelah semua pengirim selesai
}()

for h := range hasil {   // berhenti sendiri saat channel ditutup
	fmt.Println(h)
}

select

select {
case v := <-masuk:
	proses(v)
case keluar <- hasil:
	// terkirim
case <-time.After(2 * time.Second):
	return errors.New("waktu habis")
case <-ctx.Done():
	return ctx.Err()
}

select menunggu sampai salah satu case siap. Kalau beberapa siap sekaligus, satu dipilih acak โ€” sengaja, supaya tidak ada case yang kelaparan.

Hampir setiap select di kode produksi harus punya case <-ctx.Done():. Tanpa itu, goroutine-mu tidak punya cara berhenti saat permintaan dibatalkan atau saat server dimatikan โ€” dan kamu baru menyadarinya saat deploy menggantung tiga menit menunggu graceful shutdown yang tidak pernah selesai.

default: jangan menunggu sama sekali

select {
case metrik <- m:
	// terkirim
default:
	// penuh โ€” buang saja. Jangan pernah menahan permintaan pengguna
	// demi mengirim metrik.
	metrikTerbuang.Add(1)
}

Pola yang benar-benar dipakai

Worker pool berbatas

func prosesSemua(ctx context.Context, item []Item, n int) error {
	tugas := make(chan Item)
	g, ctx := errgroup.WithContext(ctx)

	for range n {                       // n pekerja โ€” BUKAN satu per item
		g.Go(func() error {
			for it := range tugas {
				if err := proses(ctx, it); err != nil {
					return err          // membatalkan ctx untuk yang lain
				}
			}
			return nil
		})
	}

	g.Go(func() error {
		defer close(tugas)
		for _, it := range item {
			select {
			case tugas <- it:
			case <-ctx.Done():
				return ctx.Err()
			}
		}
		return nil
	})

	return g.Wait()
}

Kenapa jumlah pekerja dibatasi: paralelisme yang tidak dibatasi memindahkan kemacetan ke tempat lain. Seribu goroutine yang masing-masing membuka koneksi database akan menabrak batas koneksi Postgres (biasanya 100) โ€” dan yang gagal bukan cuma pekerjaan latar, tapi juga permintaan pengguna yang sedang berjalan. Angka pekerja harus lebih kecil dari ukuran connection pool (Fase 4).

Sinyal berhenti dengan channel tertutup

berhenti := make(chan struct{})

go func() {
	for {
		select {
		case <-berhenti:
			return
		case <-tik.C:
			bersihkan()
		}
	}
}()

close(berhenti)   // menutup = memberi sinyal ke SEMUA penerima sekaligus

chan struct{} adalah channel yang tidak membawa data โ€” hanya sinyal. struct{} memakai nol byte. Menutupnya membangunkan semua penerima serentak, yang membuatnya jadi cara paling ringkas menyiarkan "berhenti" ke banyak goroutine sekaligus.

Kapan channel bukan jawabannya

KebutuhanPakai
Melindungi satu variabel bersamasync.Mutex โ€” lebih sederhana dan lebih cepat
Menghitung sesuatu dari banyak goroutinesync/atomic
Cache dibaca banyak, ditulis jarangsync.RWMutex
Menunggu sekelompok pekerjaan selesaisync.WaitGroup / errgroup
Memindahkan data antar tahap pipelineChannel
Menyiarkan sinyal berhentiChannel yang ditutup, atau context

Kesalahan pendatang yang paling umum: memakai channel untuk semuanya karena ia terasa "cara Go". Slogan resminya justru punya lanjutan yang jarang dikutip โ€” bahwa mutex sepenuhnya sah untuk melindungi keadaan bersama yang sederhana. Kalau sebuah pola dengan channel butuh diagram untuk dijelaskan, mutex hampir selalu lebih baik.

Deadlock

func main() {
	ch := make(chan int)
	ch <- 1   // fatal error: all goroutines are asleep - deadlock!
}

Runtime Go mendeteksi kasus di mana semua goroutine tertidur dan mematikan program dengan pesan yang jelas. Yang tidak terdeteksi: deadlock parsial, di mana sebagian goroutine masih berjalan. Untuk itu, alatnya go test -race dan profil goroutine.

Latihan: tulis pipeline tiga tahap โ€” pembangkit angka โ†’ penyaring bilangan prima โ†’ pencetak โ€” yang dihubungkan channel dan berhenti rapi saat ctx dibatalkan setelah dua detik. Lalu sengaja tutup channel dari sisi penerima dan baca pesan paniknya. Terakhir, hapus case <-ctx.Done() dan lihat program menggantung โ€” itu bentuk bug yang akan kamu temui lagi di Fase 3.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.