Menjalankan goroutine cukup dengan dua huruf: go. Karena itulah concurrency jadi salah satu hal pertama yang disukai orang dari Go, sekaligus sumber banyak bug produksi yang kami temui di layanan Go. Goroutine yang tidak pernah selesai, map yang ditulis dari dua tempat sekaligus, dan request yang terus berjalan padahal client sudah pergi. Pola-pola di bawah adalah yang kami ajarkan dan pakai sendiri untuk menghindari masalah itu.
Setiap Goroutine Harus Punya Jalan Keluar
Goroutine itu murah, tapi tidak akan dibersihkan selama masih tertahan. Goroutine yang menunggu selamanya di channel yang tidak akan pernah diisi tetap memegang stack dan semua data yang dirujuknya sampai proses di-restart. Sebelum menulis go func(), jawab satu pertanyaan: apa yang membuat goroutine ini selesai? Jawabannya biasanya channel yang ditutup, context yang dibatalkan, atau pekerjaan yang memang ada ujungnya. Kalau tidak ada jawaban, itu kebocoran.
Channel atau Mutex?
| Situasi | Pakai | Alasan |
|---|---|---|
| Mengirim pekerjaan atau hasil dari satu goroutine ke goroutine lain | Channel | Kepemilikan data ikut berpindah bersama nilainya, jadi hanya satu goroutine yang memegangnya dalam satu waktu |
| Memberi sinyal bahwa sesuatu sudah selesai atau harus berhenti | Channel yang ditutup atau context | Menutup channel membangunkan semua penerima sekaligus |
| Beberapa goroutine membaca dan mengubah map atau counter yang sama | sync.Mutex atau sync.RWMutex | Lebih sederhana dan lebih cepat daripada mengalirkan setiap perubahan lewat satu goroutine |
| Satu angka yang diubah dari banyak goroutine | Tipe sync/atomic seperti atomic.Int64 | Tidak perlu lock untuk satu nilai |
Pepatah Go menyarankan berbagi memori dengan cara berkomunikasi, dan channel memang pilihan awal yang bagus untuk pipeline. Tetap saja, mutex di sekitar struct kecil sering menghasilkan kode yang paling jelas. Pilih yang membuat kepemilikan data paling mudah dipahami pembaca berikutnya.
Teruskan Context ke Setiap Pemanggilan
Di server HTTP, r.Context() dibatalkan ketika client memutus koneksi atau server dimatikan. Teruskan context itu sebagai argumen pertama ke setiap fungsi yang melakukan I/O, dan pakai versi pemanggilan database dan HTTP yang menerima context, seperti QueryContext dan http.NewRequestWithContext. Dengan begitu request yang dibatalkan ikut menghentikan query database dan pemanggilan ke layanan lain, sehingga server tidak menyelesaikan pekerjaan yang hasilnya tidak akan dibaca siapa pun.
- Pasang batas waktu dengan context.WithTimeout di sekitar pemanggilan ke layanan lain, dan selalu panggil fungsi cancel dengan defer.
- Di dalam loop yang panjang, periksa ctx.Done() lewat select supaya loop bisa berhenti lebih awal.
- Jangan simpan context di dalam struct. Teruskan per pemanggilan.
- Pakai context.WithoutCancel untuk pekerjaan yang harus tetap selesai setelah response terkirim, misalnya menulis audit log.
Tunggu Goroutine dengan errgroup
sync.WaitGroup dipakai untuk menunggu goroutine, dan sejak Go 1.25 method Go-nya bisa menjalankan sekaligus mencatat goroutine dalam satu panggilan. Kalau goroutine-nya bisa gagal, golang.org/x/sync/errgroup biasanya lebih cocok. errgroup.WithContext mengembalikan sebuah group dan sebuah context. Begitu ada goroutine yang mengembalikan error, context dibatalkan supaya goroutine lain bisa berhenti, dan Wait mengembalikan error pertama.
Contoh pemakaian yang umum adalah memuat dashboard yang butuh data dari tiga layanan. Jalankan tiga g.Go, masing-masing menulis ke variabelnya sendiri, lalu panggil g.Wait. Halaman cukup menunggu selama pemanggilan yang paling lambat, dan satu kegagalan langsung membatalkan dua lainnya.
Batasi Jumlah Goroutine dengan Worker Pool
Satu goroutine per item masih aman untuk sepuluh item, tapi ambruk di seratus ribu item, biasanya karena koneksi database habis atau kena rate limit API lain. Batasi jumlah worker.
- 1Buat group dengan errgroup.WithContext lalu panggil g.SetLimit(n), dengan n disesuaikan dengan kemampuan sistem tujuan.
- 2Loop setiap item dan panggil g.Go untuk masing-masing. Go akan menunggu kalau sudah ada n goroutine yang berjalan, jadi backpressure didapat tanpa kode tambahan.
- 3Di dalam setiap goroutine, langsung kembali kalau ctx.Err() tidak nil.
- 4Kumpulkan hasil di slice berdasarkan posisi, atau kirim lewat channel yang dibaca satu goroutine.
- 5Panggil g.Wait dan tangani error-nya sekali.
Pakai Select untuk Timeout dan Pembatalan
Statement select menunggu beberapa operasi channel dan menjalankan yang paling dulu siap. Gabungkan channel hasil dengan ctx.Done() supaya goroutine bisa menyerah ketika pemanggilnya berhenti menunggu. Kalau goroutine mengirim hasil ke channel yang mungkin tidak lagi dibaca pemanggil, beri channel itu buffer satu supaya pengiriman tidak pernah tertahan dan goroutine bisa selesai.
Temukan Data Race Sebelum Terjadi di Produksi
- Jalankan test dengan go test -race di CI. Race detector hanya menemukan race di kode yang memang dijalankan, jadi hasilnya paling bagus kalau test-nya ikut menjalankan jalur yang paralel.
- Map biasa tidak aman untuk ditulis bersamaan. Go akan menghentikan program dengan error concurrent map writes. Lindungi dengan mutex.
- Tutup channel hanya dari sisi pengirim, dan cukup sekali. Mengirim ke channel yang sudah ditutup akan panic.
- Pasang recover di goroutine berumur panjang yang Anda jalankan sendiri. Panic di goroutine mana pun menghentikan seluruh program.
- Pantau jumlah goroutine di metrik. Angka yang terus naik walaupun trafik sudah turun menandakan kebocoran, dan profil goroutine dari net/http/pprof menunjukkan di mana mereka tertahan.
Satu Bug yang Sudah Hilang
Sebelum Go 1.22, loop for memakai satu variabel yang sama untuk semua iterasi, sehingga goroutine yang dijalankan di dalam loop sering melihat nilai terakhir saja. Sejak Go 1.22, setiap iterasi punya variabelnya sendiri asalkan go.mod mencantumkan go 1.22 atau lebih baru. Kode lama masih banyak berisi trik item := item, dan trik itu aman dihapus setelah upgrade.
Menulis kode paralel di Go itu mudah. Pekerjaan sesungguhnya ada pada membuatnya berhenti dengan rapi.
Pelatihan Golang kami memakai satu hari penuh untuk pola-pola ini, dengan latihan yang sengaja dibuat bocor, race, dan deadlock, supaya engineer mengenali gejalanya sebelum menemuinya di produksi.
Poin penting
- Pastikan tahu apa yang membuat goroutine selesai sebelum menjalankannya, kalau tidak goroutine itu akan bocor.
- Pakai channel untuk serah terima data dan sinyal, lalu mutex atau atomic untuk state bersama.
- Teruskan context sebagai argumen pertama ke setiap pemanggilan I/O supaya pembatalan sampai ke database dan layanan lain.
- Pakai errgroup dengan SetLimit untuk menunggu goroutine, membatalkan saat error pertama, dan membatasi jumlahnya.
- Jalankan go test -race di CI dan pantau jumlah goroutine di produksi.


