Goroutine dan Channel: Model Konkursi di Go
Go menjadikan konkurensi bagian dari bahasa, bukan pustaka eksternal. Anda cukup menambahkan kata kunci go untuk menjalankan fungsi secara bersamaan, lalu memakai channel untuk mengirim data di antara keduanya. Artikel ini menjelaskan cara kerja goroutine dan channel lewat pola worker pool, select, serta deteksi data race dengan contoh yang diuji memakai Go 1.27.1.
Ringkasan
- Goroutine adalah fungsi yang berjalan bersamaan dan ringan.
- Channel mengirim nilai sekaligus menyinkronkan dua goroutine.
- Unbuffered memaksa pertukaran; buffered membatasi kecepatan.
- Worker pool membagi pekerjaan ke jumlah goroutine tetap.
selectmenunggu beberapa channel sekaligus atau memberi timeout.- Race detector menemukan akses konkuren yang tidak aman.
Goroutine: Fungsi yang Berjalan Bersamaan
Goroutine adalah fungsi yang berjalan bersamaan dengan goroutine lain di ruang alamat yang sama. Cukup beri awalan go pada pemanggilan fungsi:
go hitung()
go kirimEmail()Sesuai uraian Effective Go, goroutine itu ringan: biayanya “tidak jauh lebih mahal daripada alokasi ruang stack”. Stack dimulai kecil lalu tumbuh sesuai kebutuhan, sehingga Anda bisa menjalankan ribuan goroutine tanpa takut kehabisan memori. Runtime memultipleks goroutine ke beberapa thread sistem operasi, dan jika satu goroutine terblok menunggu I/O, goroutine lain tetap berjalan.
Saat fungsi selesai, goroutine berhenti secara diam-diam. Tidak ada nilai balik dan tidak ada pesan error, sehingga komunikasi hasil dilakukan lewat channel, bukan nilai kembalian.
Konkursi vs Paralelisme
Rob Pike membedakan keduanya dalam tulisan klasiknya. Konkursi adalah cara menyusun banyak tugas yang maju secara independen; paralelisme adalah eksekusi beberapa tugas pada saat yang benar-benar bersamaan, yang umumnya butuh banyak inti CPU. Go memudahkan komposisi tugas, sedangkan paralelisme hanyalah hasil sampingan bila perangkat keras mengizinkan. Keduanya tidak saling meniadakan: program kompetitif bisa berjalan berkelindan atau paralel, dan tetap benar dalam kedua kasus.
Channel: Mengirim Nilai sekaligus Menyerempakkan
Channel dibuat dengan make. Tanpa argumen kedua, channel bersifat unbuffered:
ch := make(chan int) // unbuffered
buf := make(chan string, 5) // buffered, kapasitas limaPerbedaan prinsipil antara keduanya menentukan kapan pengirim atau penerima menunggu.
| Mode | Perilaku | Cocok untuk |
|---|---|---|
Unbuffered make(chan T) | Pengirim terblok sampai penerima mengambil nilainya | Sinkronisasi pertukaran satu-satu |
Buffered make(chan T, n) | Pengirim terblok hanya saat buffer penuh | Antrean pekerjaan, pembatas laju |
Channel unbuffered menggabungkan komunikasi dan sinkronisasi: pengirim memblokir sampai penerima mengambil nilainya, dan penerima memblokir sampai ada nilai yang tersedia. Dua goroutine bertemu di titik itu dalam kondisi yang saling diketahui. Inilah inti pepatah Go: “do not communicate by sharing memory; instead, share memory by communicating”. Karena nilai berpindah dari satu goroutine ke goroutine lain, hanya satu goroutine yang memegang akses pada satu waktu.
Berikut pola sederhana menunggu tugas selesai:
selesai := make(chan struct{})
go func() {
lakukanPekerjaanBerat()
close(selesai) // menandakan selesai tanpa mengirim nilai
}()
<-selesai // memblokir sampai channel ditutupChannel struct{} tanpa isi dipakai sebagai sinyal: menutupnya membangunkan penerima yang menunggu. Channel buffered lebih longgar: pengirim hanya memblokir saat buffer penuh, yang berguna untuk membatasi jumlah proses bersamaan, misalnya antrean pekerjaan. Detail lengkap ada di Tour of Go, bagian channels.
Arah Channel
Tipe channel bisa membatasi arah operasi. Pada kode worker pool nanti, parameter jobs <-chan int berarti fungsi hanya boleh membaca, sedangkan results chan<- int hanya boleh menulis. Batasan ini dicegah oleh kompilator dan membuat kontrak fungsi lebih jelas.
Pola Worker Pool: Fan-out dan Fan-in
Pola paling umum adalah worker pool. Beberapa goroutine pekerja membaca dari satu channel pekerjaan, lalu hasilnya dikumpulkan di channel lain. Program berikut valid dijalankan dengan Go 1.27, lolos gofmt, dan bersih dari go vet:
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
time.Sleep(10 * time.Millisecond)
fmt.Printf("worker %d memproses job %d\n", id, j)
results <- j * j
}
}
func main() {
const numJobs = 6
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // tidak ada lagi pekerjaan
wg.Wait() // tunggu semua worker selesai
close(results)
for r := range results {
fmt.Println("hasil:", r)
}
}Ketika dijalankan, hasilnya mirip dengan ini:
worker 2 memproses job 3
worker 1 memproses job 2
worker 3 memproses job 1
worker 2 memproses job 4
worker 1 memproses job 5
worker 3 memproses job 6
hasil: 9
hasil: 4
hasil: 1
hasil: 16
hasil: 25
hasil: 36
Urutan baris bergantung pada jadwal runtime dan berubah di setiap eksekusi. close(jobs) menandakan tidak ada lagi pekerjaan sehingga loop range di setiap worker berhenti; WaitGroup memastikan main tidak berakhir sebelum semua worker selesai. Konsep dasarnya dijelaskan di Tour of Go, bagian goroutines. Program yang toleran terhadap urutan acak adalah syarat mutlak; jangan mengandalkan worker tertentu mengerjakan job tertentu.
Select: Menunggu Beberapa Channel Sekaligus
select membuat goroutine menunggu beberapa operasi channel sekaligus dan menjalankan kasus yang lebih dulu siap. Idiom timeout memakai time.After:
package main
import (
"fmt"
"time"
)
func main() {
ping := make(chan string)
go func() {
time.Sleep(500 * time.Millisecond)
ping <- "pong"
}()
select {
case msg := <-ping:
fmt.Println("terima:", msg)
case <-time.After(100 * time.Millisecond):
fmt.Println("timeout, tak ada balasan")
}
}Pengiriman baru terjadi setelah 500 milidetik, sehingga program ini mencetak:
timeout, tak ada balasanBila beberapa kasus siap bersamaan, select memilih secara acak, yang menjaga keadilan di antara pilihan. Pola umum yang memanfaatkan select terangkum di tabel berikut.
| Pola | Fungsi | Bentuk |
|---|---|---|
| Timeout | Memberi batas waktu pada operasi yang lambat | case <-time.After(...) |
| Multi-sumber | Menunggu data pertama dari beberapa channel | Beberapa case baca sekaligus |
| Sinyal berhenti | Jalan keluar agar goroutine tak terblok selamanya | Channel done yang ditutup |
Channel atau Mutex?
Channel bukan jawaban untuk semua kasus. Effective Go mengingatkan bahwa pendekatan “berbagi lewat komunikasi” bisa berlebihan; penghitung yang dibagikan, misalnya, lebih jelas disinkronkan dengan mutex. Ringkasnya, pilih alat berdasarkan bentuk pekerjaannya.
| Alat | Cocok untuk |
|---|---|
| Channel | Aliran nilai antar goroutine, orkestrasi, sinyal selesai |
| WaitGroup | Menunggu sekumpulan goroutine tuntas |
| Mutex | Melindungi data bersama yang sederhana, misalnya penghitung |
Pertanyaan praktisnya: apakah nilai itu bergerak, atau dibagi dan dimutasi? Nilai yang bergerak lewat channel; data bersama yang berubah di tempat memakai mutex.
Data Race dan Race Detector
Channel membuat banyak pola aman, tetapi dua goroutine tetap bisa mengakses variabel yang sama secara langsung. Akibatnya adalah data race: hasil bergantung pada urutan eksekusi yang tidak bisa diprediksi. Program berikut menaikkan total dari seribu goroutine tanpa sinkronisasi:
var total int
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
total++
}()
}
wg.Wait()
fmt.Println("total:", total)Pada mesin uji saat menulis artikel ini, hasilnya 975, bukan 1000. Angka itu berubah-ubah di tiap eksekusi, dan itulah tanda data race. Perbaikan termudah adalah mutex:
var mu sync.Mutex
go func() {
defer wg.Done()
mu.Lock()
total++
mu.Unlock()
}()Dengan mutex, hasil konsisten 1000. Jangan mengandalkan pengamatan biasa, karena hasil yang salah bisa bertahan lama. Jalankan race detector dengan go run -race; detektor melaporkan WARNING: DATA RACE beserta lokasi baca-tulis yang bertabrakan, seperti dijelaskan pada dokumentasi race detector. Lihat siapa yang membaca dan menulis variabel, lalu pilih channel atau mutex yang tepat.
Kesimpulan
Goroutine memberi eksekusi bersamaan yang ringan, dan channel menjadi jembatan yang mengubah komunikasi menjadi sinkronisasi secara alami. Mulailah dari pola worker pool dengan channel, tambahkan select untuk kontrol waktu, lalu pasang race detector di lini integrasi Anda. Model ini adalah jantung pemrograman Go dan tetap stabil sejak versi pertamanya.