Memahami Goroutine dan Channel untuk Konkurensi di Go
Pendahuluan
Bayangkan kamu sedang membangun backend untuk aplikasi ride-hailing seperti[7D[K seperti Gojek — ratusan ribu permintaan masuk setiap detik: cek posisi driv[4D[K driver, kalkulasi tarif, kirim notifikasi, simpan log. Jika setiap perminta[8D[K permintaan harus menunggu yang sebelumnya selesai, sistemmu akan kolaps dal[3D[K dalam hitungan menit.
Di sinilah konkurensi menjadi kunci. Go dirancang sejak awal dengan kon[3D[K konkurensi sebagai fitur inti, bukan tambahan. Dua mekanisme utamanya — **g[3D[K goroutine dan channel — memungkinkan kamu menulis kode konkuren yan[3D[K yang bersih, efisien, dan mudah dibaca.
Artikel ini mengasumsikan kamu sudah familiar dengan sintaks dasar Go. Kita[4D[K Kita akan langsung masuk ke pola-pola praktis yang dipakai di production.
Apa Itu Konkurensi di Go?
Konkurensi bukan berarti dua hal terjadi tepat bersamaan (itu paralelisme[11D[K paralelisme). Konkurensi adalah kemampuan program untuk mengelola banyak [K pekerjaan sekaligus — meskipun di satu waktu hanya satu yang berjalan.
Go menerapkan model konkurensi berbasis CSP (Communicating Sequential Pro[3D[K Processes). Prinsipnya sederhana:
“Jangan berkomunikasi dengan berbagi memori; sebaliknya, bagikan memori [K dengan berkomunikasi.”
Ini berbeda dari pendekatan thread tradisional di Java atau C++ yang mengan[6D[K mengandalkan mutex dan shared memory. Kalau kamu terbiasa dengan konsep sep[3D[K seperti [Inheritance dan Polymorphism di C++](/docs/sw/cpp/inheritance-dan-[34DK C++ yang fokus pada struktu[7D[K struktur objek, cara berpikir Go ini membutuhkan sedikit pergeseran mental.[7D[K mental.
Goroutine: Konkurensi Ringan di Go
Goroutine adalah fungsi yang berjalan secara konkuren dengan fungsi lai[3D[K
lain. Cukup tambahkan kata kunci go sebelum pemanggilan fungsi.
package main
import (
"fmt"
"sync"
"time"
)
func cetakPesan(pesan string, wg *sync.WaitGroup) {
defer wg.Done() // Menandai bahwa goroutine ini sudah selesai
for i := 0; i < 3; i++ {
fmt.Println(pesan)
time.Sleep(100 * time.Millisecond) // Simulasi jeda kerja
}
}
func main() {
var wg sync.WaitGroup // Digunakan untuk menunggu semua goroutine selesai
wg.Add(2) // Ada 2 goroutine yang akan dijalankan
go cetakPesan("Goroutine pertama", &wg) // Menjalankan fungsi secara bersam[6D[K
bersamaan
go cetakPesan("Goroutine kedua", &wg)
wg.Wait() // Main menunggu sampai semua goroutine selesai
fmt.Println("Main selesai")
}
/*
# Output yang diharapkan:
# > Goroutine pertama
# > Goroutine kedua
# > Goroutine pertama
# > Goroutine kedua
# > Goroutine pertama
# > Goroutine kedua
# > Main selesai
#
# Catatan:
# Urutan dua goroutine bisa berbeda-beda saat dijalankan, karena scheduler [K
Go
# dapat mengeksekusinya secara bergantian.
*/
Output akan menampilkan pesan dari kedua goroutine yang bercampur — itulah [K konkurensi bekerja.
Goroutine vs Thread OS
| Aspek | Thread OS | Goroutine |
|---|---|---|
| Ukuran stack awal | ~2 MB | ~2 KB (tumbuh dinamis) |
| Biaya pembuatan | Mahal | Sangat murah |
| Jumlah praktis | Ribuan | Jutaan |
| Penjadwalan | OS | Go runtime (M:N scheduling) |
Go runtime memetakan banyak goroutine ke sejumlah kecil thread OS. Inilah k[1D[K kenapa kamu bisa spawn jutaan goroutine tanpa kehabisan memori — sesuatu ya[2D[K yang tidak mungkin dengan thread konvensional.
Masalah dengan time.Sleep
Menggunakan time.Sleep untuk menunggu goroutine adalah praktik buruk di p[1D[K
production. Solusinya adalah sync.WaitGroup:
package main
import (
"fmt"
"sync"
"time"
)
func prosesData(id int, wg *sync.WaitGroup) {
defer wg.Done() // Menandai goroutine ini selesai saat fungsi berakhir
fmt.Printf("Memproses data %d\n", id)
// Simulasi pekerjaan singkat agar contoh terasa lebih realistis
time.Sleep(200 * time.Millisecond)
fmt.Printf("Data %d selesai diproses\n", id)
}
func main() {
var wg sync.WaitGroup // WaitGroup dipakai untuk menunggu semua goroutine s[1D[K
selesai
for i := 1; i <= 5; i++ {
wg.Add(1) // Tambah counter sebelum menjalankan goroutine
go prosesData(i, &wg) // Jalankan prosesData secara concurrent
}
wg.Wait() // Menunggu sampai semua goroutine selesai
fmt.Println("Semua data selesai diproses")
}
/*
# Output yang diharapkan:
# > Memproses data 1
# > Memproses data 2
# > Memproses data 3
# > Memproses data 4
# > Memproses data 5
# > Data 1 selesai diproses
# > Data 2 selesai diproses
# > Data 3 selesai diproses
# > Data 4 selesai diproses
# > Data 5 selesai diproses
# > Semua data selesai diproses
#
# Catatan:
# > Urutan output bisa berbeda karena goroutine berjalan secara concurrent.[11D[K
concurrent.
*/
Channel: Komunikasi Aman Antar Goroutine
Channel adalah “pipa” yang memungkinkan goroutine saling mengirim dan m[1D[K
menerima nilai secara aman. Channel di Go bersifat type-safe — channel i[2D[K inthanya bisa membawa nilaiint`.
package main
import "fmt"
func main() {
unbuffered := make(chan int) // Channel tanpa buffer: pengirim dan pener[5D[K
penerima harus siap di saat yang sama
buffered := make(chan int, 5) // Channel dengan buffer: bisa menampung sa[2D[K
sampai 5 nilai
go func() {
unbuffered <- 10 // Kirim nilai ke channel unbuffered dari goroutine terpis[6D[K
terpisah
}()
nilai := <-unbuffered // Terima nilai dari channel unbuffered
buffered <- 20 // Simpan nilai pertama ke channel buffered
buffered <- 30 // Simpan nilai kedua ke channel buffered
fmt.Println("Unbuffered:", nilai)
fmt.Println("Buffered:", <-buffered, <-buffered)
}
/*
# Output yang diharapkan:
# > Unbuffered: 10
# > Buffered: 20 30
*/
Unbuffered Channel
Pada unbuffered channel, pengirim memblokir sampai ada penerima, dan se[2D[K sebaliknya. Ini seperti serah-terima langsung — kedua pihak harus hadir ber[3D[K bersamaan.
package main
import "fmt"
// kirimNilai mengirim satu data ke channel.
func kirimNilai(ch chan<- int) {
ch <- 42 // Kirim nilai 42 ke channel
}
func main() {
ch := make(chan int) // Buat channel untuk data bertipe int
go kirimNilai(ch) // Jalankan pengiriman data dalam goroutine
nilai := <-ch // Terima data dari channel
fmt.Println("Diterima:", nilai)
}
/*
# Output yang diharapkan:
# > Diterima: 42
*/
Buffered Channel
Buffered channel memungkinkan pengirim menaruh beberapa nilai tanpa harus l[1D[K langsung diambil — seperti antrian pesan.
package main
import "fmt"
func main() {
// Membuat channel bertipe string dengan buffer berkapasitas 3.
ch := make(chan string, 3)
// Mengirim tiga pesan ke channel.
// Karena channel memiliki buffer 3, pengiriman ini tidak langsung membloki[8D[K
memblokir.
ch <- "pesan pertama"
ch <- "pesan kedua"
ch <- "pesan ketiga"
// Menerima dan menampilkan isi channel sesuai urutan pengiriman.
fmt.Println(<-ch)
fmt.Println(<-ch)
fmt.Println(<-ch)
}
/*
# Output yang diharapkan:
# > pesan pertama
# > pesan kedua
# > pesan ketiga
*/
Menutup Channel dan Iterasi
package main
import "fmt"
// generator mengirim angka 1 sampai 5 ke channel, lalu menutupnya.
func generator(ch chan<- int) {
for i := 1; i <= 5; i++ {
ch <- i // Kirim nilai ke channel
}
close(ch) // Tutup channel agar range di main berhenti
}
func main() {
ch := make(chan int, 5) // Buffer 5 agar pengiriman tidak langsung blocking[8D[K
blocking
go generator(ch) // Jalankan generator sebagai goroutine
for nilai := range ch { // Baca data sampai channel ditutup
fmt.Println(nilai)
}
}
/*
# Output yang diharapkan:
# > 1
# > 2
# > 3
# > 4
# > 5
*/
Menggabungkan Goroutine dan Channel
Pola paling umum adalah pipeline: satu goroutine menghasilkan data, dik[3D[K dikirim via channel, lalu diproses goroutine lain.
package main
import "fmt"
// generate mengirim setiap angka ke channel, lalu menutup channel saat sel[3D[K
selesai.
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n // kirim angka satu per satu ke tahap berikutnya
}
close(out) // tutup channel agar receiver tahu data sudah habis
}()
return out
}
// kuadrat menerima angka dari channel input, lalu mengirim hasil kuadratny[9D[K
kuadratnya.
func kuadrat(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n // proses setiap angka
}
close(out) // tutup channel output setelah semua data diproses
}()
return out
}
func main() {
// bangun pipeline: generate -> kuadrat
angka := generate(2, 3, 4, 5)
hasil := kuadrat(angka)
// baca dan tampilkan semua hasil dari pipeline
for v := range hasil {
fmt.Println(v)
}
}
/*
# Output yang diharapkan:
# > 4
# > 9
# > 16
# > 25
*/
Pola pipeline ini mirip dengan cara kita memproses data dalam [Menguasai Ar[2D[K Array dan Operasi Dasar dalam Struktur Data](/docs/cs/data-structures/mengu[36DK Data — [K hanya saja di sini data mengalir secara konkuren melalui beberapa tahap pem[3D[K pemrosesan.
Pola Konkurensi Umum dengan Select
select memungkinkan goroutine menunggu di beberapa channel sekaligus — me[2D[K
mengambil yang pertama kali siap.
package main
import (
"fmt"
"time"
)
func main() {
// Membuat dua channel untuk menerima pesan dari goroutine yang berbeda.
ch1 := make(chan string)
ch2 := make(chan string)
// Goroutine pertama mengirim pesan setelah 1 detik.
go func() {
time.Sleep(1 * time.Second)
ch1 <- "Pesan dari channel 1"
}()
// Goroutine kedua mengirim pesan setelah 2 detik.
go func() {
time.Sleep(2 * time.Second)
ch2 <- "Pesan dari channel 2"
}()
// Menunggu dua pesan masuk, lalu menampilkan pesan yang datang lebih dulu.[5D[K
dulu.
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println(msg)
case msg := <-ch2:
fmt.Println(msg)
}
}
}
/*
# Output yang diharapkan:
# > Pesan dari channel 1
# > Pesan dari channel 2
*/
Timeout dengan Select
Pola timeout sangat penting di production untuk menghindari goroutine yang [K menggantung selamanya:
package main
import (
"fmt"
"time"
)
// ambilDataDariAPI mensimulasikan proses request ke API yang lambat.
func ambilDataDariAPI(ch chan<- string) {
time.Sleep(3 * time.Second) // Simulasi respons API selama 3 detik
ch <- "data berhasil" // Kirim hasil ke channel
}
func main() {
ch := make(chan string, 1) // Channel buffer untuk menerima hasil dari goro[4D[K
goroutine
go ambilDataDariAPI(ch) // Jalankan proses API secara concurrent
select {
case hasil := <-ch:
// Jika data datang lebih cepat dari batas waktu, tampilkan hasilnya
fmt.Println("Berhasil:", hasil)
case <-time.After(2 * time.Second):
// Jika menunggu lebih dari 2 detik, anggap request timeout
fmt.Println("Timeout! API tidak merespons dalam 2 detik")
}
}
/*
# Output yang diharapkan:
# > Timeout! API tidak merespons dalam 2 detik
*/
Contoh Kasus Nyata: Worker Pool
Jika kamu ingin membangun layanan pemrosesan pesanan seperti Tokopedia atau[4D[K atau Shopee, kamu perlu memproses ribuan pesanan masuk secara efisien. **Wo[4D[K Worker pool adalah pola klasik untuk ini: sejumlah worker goroutine sia[3D[K siap mengambil pekerjaan dari satu antrian.
package main
import (
"fmt"
"sync"
"time"
)
// Pesanan merepresentasikan satu data pesanan.
type Pesanan struct {
ID int
Produk string
}
// worker menerima pesanan dari channel lalu memprosesnya satu per satu.
func worker(id int, antrian <-chan Pesanan, wg *sync.WaitGroup) {
defer wg.Done() // Menandai bahwa worker selesai saat fungsi berakhir.
for pesanan := range antrian {
fmt.Printf("Worker %d memproses pesanan #%d (%s)\n", id, pesanan.ID, pesana[6D[K
pesanan.Produk)
time.Sleep(100 * time.Millisecond) // Simulasi waktu pemrosesan.
}
}
func main() {
const jumlahWorker = 3
const jumlahPesanan = 10
// Channel buffered agar beberapa pesanan bisa masuk tanpa langsung menungg[7D[K
menunggu worker.
ch := make(chan Pesanan, jumlahPesanan)
// Membuat worker goroutine.
for i := 1; i <= jumlahWorker; i++ {
go worker(i, ch, &wg)
}
// Mengirim pesanan ke channel.
for i := 1; i <= jumlahPesanan; i++ {
ch <- Pesanan{ID: i, Produk: fmt.Sprintf("Produk %d", i)}
}
close(ch) // Tutup channel setelah semua pesanan dikirim.
wg.Wait() // Menunggu semua worker selesai.
fmt.Println("Semua pesanan diproses.")
}
/*
# Output yang diharapkan:
# > Worker 1 memproses pesanan #1 (Produk 1)
# > Worker 2 memproses pesanan #2 (Produk 2)
# > Worker 3 memproses pesanan #3 (Produk 3)
# > Worker 1 memproses pesanan #4 (Produk 4)
# > Worker 2 memproses pesanan #5 (Produk 5)
# > Worker 3 memproses pesanan #6 (Produk 6)
# > Worker 1 memproses pesanan #7 (Produk 7)
# > Worker 2 memproses pesanan #8 (Produk 8)
# > Worker 3 memproses pesanan #9 (Produk 9)
# > Worker 1 memproses pesanan #10 (Produk 10)
# > Semua pesanan diproses.
*/
Kesimpulan
Goroutine dan channel adalah jantung dari konkurensi di Go. Keduanya bekerj[6D[K bekerja bersama dengan cara yang elegan:
- Goroutine menyediakan unit eksekusi konkuren yang sangat ringan — kam[3D[K kamu bisa membuat ribuan tanpa khawatir overhead.
- Channel menyediakan cara aman dan eksplisit untuk goroutine berkomuni[9D[K berkomunikasi, menghilangkan kebutuhan akan lock yang rumit.
- Select memungkinkan penanganan beberapa channel sekaligus, termasuk p[1D[K pola timeout yang krusial di production.
- Worker pool adalah pola fundamental yang menggabungkan keduanya untuk[5D[K untuk pemrosesan beban tinggi yang efisien.
Filosofi Go — “bagikan memori dengan berkomunikasi” — membuat kode konkuren[8D[K konkuren lebih mudah ditulis dan didebug dibanding pendekatan thread tradis[6D[K tradisional. Mulai dari pola sederhana seperti contoh di atas, lalu tingkat[7D[K tingkatkan ke pola yang lebih kompleks sesuai kebutuhan sistemmu.