Goroutine dan Channel: Concurrency di Go untuk Pemula
Goroutine adalah superpower Go. Pelajari cara membuat goroutine, channel, dan pattern worker pool dalam 15 menit — lengkap dengan contoh kode yang bisa langsung dijalankan.
Pernah bayangkan server yang melayani ribuan request bersamaan tanpa harus kelola thread pool sendiri? Itulah yang goroutine bisa lakukan. Di artikel ini kamu akan belajar goroutine dan channel dari nol — tidak perlu background concurrency sebelumnya.
Apa itu Goroutine?
Goroutine adalah lightweight thread yang dikelola Go runtime, bukan OS. Membuat goroutine baru hanya butuh 2 KB stack awal — bandingkan dengan OS thread yang butuh 1–8 MB.
Karena itu, kamu bisa menjalankan ribuan goroutine bersamaan tanpa kehabisan memori. Ini bukan exaggeration — aplikasi Go production sering memiliki ratusan ribu goroutine aktif sekaligus.
Perbandingan Goroutine vs OS Thread
| Goroutine | OS Thread | |
|---|---|---|
| Stack awal | 2 KB | 1–8 MB |
| Switching cost | ~200 ns | ~1 μs |
| Managed by | Go runtime | OS kernel |
| Batas praktis | 100.000+ | ~10.000 |
Untuk membuat goroutine, tambahkan keyword go sebelum pemanggilan fungsi:
1package main
2
3import (
4 "fmt"
5 "sync"
6)
7
8func sayHello(name string, wg *sync.WaitGroup) {
9 defer wg.Done()
10 fmt.Printf("Halo, %s!\n", name)
11}
12
13func main() {
14 var wg sync.WaitGroup
15 names := []string{"Santekno", "Pembaca", "Gopher"}
16
17 for _, name := range names {
18 wg.Add(1)
19 go sayHello(name, &wg) // jalankan sebagai goroutine
20 }
21 wg.Wait() // tunggu semua goroutine selesai
22}time.Sleep untuk menunggu goroutine di production. Pakai sync.WaitGroup seperti contoh di atas, atau channel untuk sinkronisasi eksplisit.Channel: Pipa Komunikasi antar Goroutine
Don’t communicate by sharing memory; share memory by communicating. — Rob Pike
Channel adalah mekanisme komunikasi goroutine di Go. Dibanding shared memory (mutex), channel membuat aliran data lebih eksplisit dan mudah di-reason.
Channel adalah pipa yang menghubungkan goroutine. Kirim data dengan operator <- dan terima dengan <-:
1// Buat buffered channel berkapasitas 1
2ch := make(chan string, 1)
3
4go func() {
5 ch <- "halo dari goroutine" // kirim ke channel
6}()
7
8msg := <-ch // terima dari channel
9fmt.Println(msg) // Output: halo dari goroutineJenis Channel
Ada tiga varian channel yang perlu kamu tahu:
- Unbuffered — pengirim menunggu penerima siap. Cocok untuk sinkronisasi ketat.
- Buffered — pengirim tidak menunggu selama buffer belum penuh. Cocok untuk decoupling ringan.
- Directional — membatasi operasi (kirim atau terima saja) di signature fungsi.
| Jenis | Sintaks | Kapan dipakai |
|---|---|---|
| Unbuffered | make(chan T) | Sinkronisasi ketat antara goroutine |
| Buffered | make(chan T, N) | Decoupling, N item bisa antre |
| Send-only | chan<- T | Fungsi hanya boleh kirim |
| Receive-only | <-chan T | Fungsi hanya boleh terima |
Diagram: Arsitektur Goroutine
flowchart LR
Main["main()"] -- "go func()" --> G1["Goroutine 1"]
Main -- "go func()" --> G2["Goroutine 2"]
Main -- "go func()" --> G3["Goroutine 3"]
G1 -- "ch <- data" --> Ch[("Channel")]
G2 -- "ch <- data" --> Ch
G3 -- "ch <- data" --> Ch
Ch -- "<-ch" --> Consumer["Consumer\ngoroutine"]
Pattern: Worker Pool
Worker pool adalah pattern untuk membatasi concurrency — misal, maksimal 5 request HTTP berjalan bersamaan:
1func workerPool(numWorkers int, jobs <-chan int, results chan<- int) {
2 var wg sync.WaitGroup
3 for i := 0; i < numWorkers; i++ {
4 wg.Add(1)
5 go func() {
6 defer wg.Done()
7 for job := range jobs {
8 results <- processJob(job)
9 }
10 }()
11 }
12 go func() {
13 wg.Wait()
14 close(results) // tutup channel setelah semua worker selesai
15 }()
16}errgroup
dari golang.org/x/sync untuk worker pool dengan error handling yang lebih ergonomis.Perhatikan baris kritis di bawah — parameter hl="3,8-10" menyorot baris 3 dan baris 8–10:
1func workerPool(numWorkers int, jobs <-chan int, results chan<- int) {
2 var wg sync.WaitGroup
3 sem := make(chan struct{}, numWorkers) // semaphore membatasi concurrency
4 for job := range jobs {
5 wg.Add(1)
6 go func(j int) {
7 defer wg.Done()
8 sem <- struct{}{} // acquire slot
9 results <- processJob(j) // kerja utama
10 <-sem // release slot
11 }(job)
12 }
13 wg.Wait()
14 close(results)
15}Select: Multiplex Channel
select seperti switch tapi untuk operasi channel — menunggu channel mana yang siap lebih dulu:
1ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
2defer cancel()
3
4select {
5case msg := <-ch1:
6 fmt.Println("dari ch1:", msg)
7case msg := <-ch2:
8 fmt.Println("dari ch2:", msg)
9case <-ctx.Done():
10 fmt.Println("timeout atau dibatalkan:", ctx.Err())
11}Pola select dengan default
Pakai default clause untuk non-blocking check:
1select {
2case msg := <-ch:
3 fmt.Println("ada data:", msg)
4default:
5 fmt.Println("tidak ada data, lanjut...")
6}select {} tanpa default atau case yang bisa selesai — itu menyebabkan goroutine block selamanya dan Checklist Sebelum Deploy Goroutine ke Production
- Goroutine memang perlu berjalan paralel (bukan sekadar bisa)
- Ada mekanisme sinkronisasi:
WaitGroup, channel, atauerrgroup - Goroutine punya jalan keluar yang jelas — tidak akan berjalan selamanya
- Context cancellation diteruskan ke semua goroutine child
- Error dari goroutine dikembalikan ke caller
- Race condition dicek dengan
go test -race - Goroutine count dipantau di production (misal via
pprof)
Ringkasan
Goroutine dan channel membuat concurrency di Go terasa natural. Tiga hal yang perlu diingat:
- Goroutine ringan — buat sebanyak yang diperlukan, runtime yang mengatur penjadwalannya
- Channel untuk komunikasi — hindari shared memory jika bisa diganti channel
- Context untuk shutdown — selalu pakai
context.Contextdi goroutine production
Selanjutnya, di artikel berikutnya kita akan masuk ke error handling idiomatic di Go — cara Go menangani kesalahan tanpa exception.
Apakah ada pertanyaan? Tinggalkan komentar di bawah atau hubungi via halaman tentang saya .