Go Goroutine dan Context
Definisi
Concurrency di Go adalah cara menyusun program supaya beberapa pekerjaan bisa berjalan tumpang tindih. Go menyediakan beberapa alat utama untuk itu:
- goroutine untuk menjalankan fungsi secara concurrent
- channel untuk mengirim data atau sinyal antar goroutine
- select untuk menunggu beberapa channel sekaligus
- worker pattern untuk membatasi dan mengatur pemrosesan paralel
- context cancellation untuk menghentikan pekerjaan yang sudah tidak relevan
Concurrency bukan berarti selalu lebih cepat. Tujuan utamanya adalah mengatur alur kerja yang melibatkan banyak tugas, banyak I/O, atau proses yang harus berjalan bersamaan tanpa saling mengunci.
Kapan concurrency dipakai
Concurrency cocok saat pekerjaan bisa dipisah menjadi beberapa unit yang tidak harus menunggu satu sama lain secara ketat.
Cocok untuk concurrency
- request ke beberapa service eksternal
- baca banyak file atau objek storage
- proses pipeline data
- streaming event atau message
- background task yang bisa berjalan paralel
- fan-out / fan-in processing
- polling, retry, dan timeout handling
Kurang cocok untuk concurrency
- kerja kecil yang selesai sangat cepat
- code path yang sederhana dan mudah dibaca secara sinkron
- tugas yang butuh urutan ketat tanpa overlap
- proses CPU-bound kecil yang overhead concurrency-nya lebih besar dari manfaatnya
Aturan praktisnya:
- Pakai concurrency kalau ada I/O waiting, banyak task independen, atau pipeline yang jelas.
- Jangan pakai concurrency hanya karena terlihat “lebih advanced”.
- Ukur dulu kalau beban kerja kecil; kadang kode sinkron lebih stabil dan lebih mudah dirawat.
Alur dasar goroutine
Goroutine adalah fungsi yang dijalankan secara concurrent oleh runtime Go.
go doWork()Kalau sebuah fungsi dipanggil biasa, eksekusinya berjalan urut di goroutine utama.
Kalau dipanggil dengan go, fungsi itu dijadwalkan untuk jalan sendiri.
Alur sederhananya
Yang perlu diingat:
- goroutine tidak otomatis menunggu goroutine lain selesai
- kalau
mainselesai lebih dulu, semua goroutine lain ikut berhenti - koordinasi selesai/tidak selesai harus dibuat eksplisit
Contoh goroutine sederhana
package main
import (
"fmt"
"time"
)
func worker(id int) {
fmt.Println("worker start", id)
time.Sleep(200 * time.Millisecond)
fmt.Println("worker done", id)
}
func main() {
go worker(1)
go worker(2)
time.Sleep(500 * time.Millisecond)
fmt.Println("main done")
}Contoh di atas menunjukkan bahwa goroutine berjalan paralel secara logis, tetapi main tetap harus memberi waktu atau menunggu dengan cara yang benar. Di aplikasi nyata, jangan mengandalkan Sleep untuk sinkronisasi. Gunakan channel, sync.WaitGroup, atau context.
Channel
Channel adalah jalur komunikasi antar goroutine. Channel dipakai untuk mengirim data, sinyal selesai, atau sinyal pembatalan.
ch := make(chan int)Fungsi utama channel
- mengirim data antar goroutine
- menyusun alur producer-consumer
- memberi sinyal selesai atau error
- menghindari koordinasi manual yang rawan race
Cara kerja channel
Urutannya biasanya begini:
- satu goroutine mengirim data ke channel
- goroutine lain menerima data dari channel
- pengirim dan penerima bisa saling menunggu bila channel belum siap
- komunikasi selesai saat data berpindah atau channel ditutup
Contoh kirim-terima data
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "halo dari goroutine"
}()
msg := <-ch
fmt.Println(msg)
}Channel buffered vs unbuffered
Unbuffered channel
- kirim dan terima harus bertemu pada saat yang sama
- cocok untuk sinkronisasi langsung
- bagus untuk handoff yang jelas
Buffered channel
ch := make(chan string, 2)- bisa menampung beberapa item sebelum penerima siap
- cocok untuk antrian kecil atau smoothing burst
- tetap bisa blocking kalau buffer penuh
Kapan channel dipakai
Channel dipakai saat kamu ingin:
- mengalirkan data antar goroutine
- membangun pipeline kerja
- menyalurkan hasil dari banyak worker
- mengirim sinyal cancel, done, atau error
- menjaga ownership data lebih jelas
Jangan pakai channel kalau data cuma dipakai lokal di satu goroutine tanpa perlu koordinasi.
select
select dipakai untuk menunggu beberapa operasi channel sekaligus.
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
default:
fmt.Println("belum ada data")
}Kapan select dipakai
- menerima hasil dari beberapa channel
- menunggu channel data atau timeout
- mendengarkan
context.Done()sambil tetap memproses data - membuat loop event-driven
Alur select
Contoh timeout dengan select
package main
import (
"fmt"
"time"
)
func main() {
result := make(chan string)
go func() {
time.Sleep(2 * time.Second)
result <- "selesai"
}()
select {
case msg := <-result:
fmt.Println("hasil:", msg)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
}select sangat berguna untuk menghindari goroutine yang menggantung terlalu lama tanpa kontrol.
Worker pattern
Worker pattern dipakai untuk membagi pekerjaan ke beberapa goroutine yang memproses job dari satu sumber antrean.
Struktur umumnya:
- satu producer mengirim task ke channel
- beberapa worker mengambil task dari channel yang sama
- hasil dikumpulkan ke channel lain atau langsung disimpan
Kenapa worker pattern penting
- membatasi jumlah goroutine aktif
- mengontrol konsumsi CPU dan memory
- cocok untuk batch processing dan fan-out
- mencegah spawn goroutine tanpa batas
Alur worker pattern
Contoh worker pool sederhana
package main
import (
"fmt"
"sync"
)
type Job struct {
ID int
}
func worker(id int, jobs <-chan Job, results chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
results <- fmt.Sprintf("worker %d memproses job %d", id, job.ID)
}
}
func main() {
jobs := make(chan Job)
results := make(chan string)
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(i, jobs, results, &wg)
}
go func() {
for i := 1; i <= 5; i++ {
jobs <- Job{ID: i}
}
close(jobs)
}()
go func() {
wg.Wait()
close(results)
}()
for msg := range results {
fmt.Println(msg)
}
}Catatan implementasi
- jangan lupa
close(jobs)saat producer selesai - pastikan
resultsditutup hanya setelah semua worker selesai - batasi jumlah worker sesuai kebutuhan, bukan sebanyak mungkin
Context cancellation
context.Context dipakai untuk menyebarkan batas waktu, pembatalan, dan nilai request-scoped ke goroutine yang terkait.
Kapan context dipakai
- request HTTP yang punya deadline
- task yang harus berhenti saat user membatalkan aksi
- worker yang perlu timeout
- proses yang bergantung pada parent task
- chain pemanggilan service yang harus ikut berhenti kalau upstream gagal
Cara kerja cancellation
Contoh context cancellation
package main
import (
"context"
"fmt"
"time"
)
func fetch(ctx context.Context) error {
select {
case <-time.After(2 * time.Second):
fmt.Println("fetch selesai")
return nil
case <-ctx.Done():
return ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
if err := fetch(ctx); err != nil {
fmt.Println("error:", err)
}
}Prinsip penting context
- selalu turunkan
ctxke fungsi yang ikut bekerja - panggil
cancel()kalau context dibuat denganWithCancel,WithTimeout, atauWithDeadline - jangan simpan context di struct untuk dipakai lama-lama
- jangan pakai
context.Background()di tengah alur kalau sudah punya context dari parent
Jebakan umum
Deadlock
Deadlock terjadi saat goroutine saling menunggu tanpa ada yang bisa lanjut.
Penyebab umum:
- kirim ke channel tanpa penerima
- menerima dari channel yang tidak akan pernah diisi
selectyang semua case-nya block dan tidak ada timeout atau cancel- menunggu
WaitGroupyang counter-nya tidak pernah selesai
Contoh pola berbahaya:
ch := make(chan int)
ch <- 1 // block karena tidak ada penerimaCara menghindari:
- pastikan ada pasangan send/receive
- tutup channel di sisi pengirim, bukan sembarangan di banyak tempat
- pakai timeout atau context untuk operasi yang bisa macet
Goroutine leak
Goroutine leak terjadi saat goroutine sudah tidak dibutuhkan, tapi tetap hidup karena masih menunggu channel, network, atau sinyal yang tidak datang.
Contoh pemicu:
- goroutine menunggu receive selamanya
- worker tidak pernah dapat sinyal stop
- result channel tidak pernah dibaca sampai habis
- context tidak diteruskan ke operasi yang lama
Cara mencegah:
- selalu sediakan jalur exit
- gunakan
ctx.Done()di loop atau blocking select - pastikan semua worker punya kondisi selesai yang jelas
- baca semua channel hasil sampai selesai atau batalkan dengan benar
Race condition
Race condition terjadi saat dua goroutine mengakses data yang sama secara bersamaan tanpa proteksi yang benar.
Gejala umum:
- hasil tidak konsisten
- bug hilang-muncul
- test kadang lulus, kadang gagal
- panic atau state yang rusak di bawah beban
Cara menghindari:
- kirim data lewat channel agar ownership jelas
- gunakan
sync.Mutexkalau shared state memang perlu - gunakan
sync/atomicuntuk kasus tertentu yang sederhana - jalankan
go test -racesaat debugging
Channel yang ditutup salah
Aturan praktis:
- sender yang menutup channel
- receiver biasanya hanya membaca sampai channel habis
- jangan menutup channel yang masih mungkin dipakai sender lain
Menutup channel dua kali akan panic.
Lupa menunggu goroutine selesai
Kalau main selesai duluan, goroutine lain bisa ikut mati sebelum pekerjaan selesai. Ini sering terjadi pada contoh kecil yang terlihat “jalan”, tapi gagal di aplikasi nyata.
Gunakan:
sync.WaitGroup- channel done
- context cancellation
Pola yang sehat
1) Fan-out / fan-in
- fan-out: satu task dibagi ke beberapa worker
- fan-in: hasil dari banyak worker digabung kembali
Pola ini cocok untuk mempercepat pemrosesan paralel yang independen.
2) Pipeline
Task diproses tahap demi tahap lewat beberapa channel.
Contoh umum:
- read
- transform
- validate
- write
3) Bounded concurrency
Jangan spawn goroutine tanpa batas kalau input bisa besar. Pakai worker pool atau semaphore untuk menahan jumlah pekerjaan aktif.
4) Cancellation-aware code
Setiap loop atau operasi lama sebaiknya punya jalur berhenti:
select { case <-ctx.Done(): ... }- timeout per langkah
- cleanup sebelum return
Ringkasan cepat
- goroutine = unit eksekusi concurrent
- channel = jalur komunikasi antar goroutine
- select = menunggu beberapa channel atau timeout sekaligus
- worker pattern = membatasi dan mengatur paralelisme
- context cancellation = cara memberi batas waktu dan stop signal
Kalau kamu sedang mendesain flow concurrent di Go, mulai dari pertanyaan ini:
- Apa yang benar-benar perlu berjalan bersamaan?
- Siapa yang punya data?
- Siapa yang menutup alur kerja?
- Bagaimana cara berhenti kalau upstream gagal atau timeout?
- Bagaimana mencegah deadlock, leak, dan race?
Referensi terkait
sync.WaitGroupuntuk menunggu banyak goroutinesync.Mutexuntuk shared statesync/atomicuntuk operasi atomik tertentugo test -raceuntuk mendeteksi race conditioncontext.WithTimeoutdancontext.WithCanceluntuk pembatalan terkontrol