Home / Programming Languages, Databases, and Software Development / Goroutine dan …

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.
  • select menunggu 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
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:

go
ch := make(chan int)        // unbuffered
buf := make(chan string, 5) // buffered, kapasitas lima

Perbedaan prinsipil antara keduanya menentukan kapan pengirim atau penerima menunggu.

ModePerilakuCocok untuk
Unbuffered make(chan T)Pengirim terblok sampai penerima mengambil nilainyaSinkronisasi pertukaran satu-satu
Buffered make(chan T, n)Pengirim terblok hanya saat buffer penuhAntrean 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:

go
selesai := make(chan struct{})
go func() {
    lakukanPekerjaanBerat()
    close(selesai) // menandakan selesai tanpa mengirim nilai
}()
<-selesai // memblokir sampai channel ditutup

Channel 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:

go
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:

text
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

Alur worker pool: main mengirim job ke channel jobs, tiga pekerja membacanya, lalu hasil terkumpul di channel results
Fan-out dari channel jobs ke tiga worker, lalu fan-in ke channel results.

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:

go
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:

text
timeout, tak ada balasan

Bila beberapa kasus siap bersamaan, select memilih secara acak, yang menjaga keadilan di antara pilihan. Pola umum yang memanfaatkan select terangkum di tabel berikut.

PolaFungsiBentuk
TimeoutMemberi batas waktu pada operasi yang lambatcase <-time.After(...)
Multi-sumberMenunggu data pertama dari beberapa channelBeberapa case baca sekaligus
Sinyal berhentiJalan keluar agar goroutine tak terblok selamanyaChannel 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.

AlatCocok untuk
ChannelAliran nilai antar goroutine, orkestrasi, sinyal selesai
WaitGroupMenunggu sekumpulan goroutine tuntas
MutexMelindungi 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:

go
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:

go
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.