Belajar Golang dari Nol #10: Goroutine dan Channel
Selamat datang lagi. Di Belajar Golang dari Nol #9 kita sudah kenalan dengan interface, cara Go membuat kode jadi fleksibel. Sampai bagian itu, semua program yang kita tulis berjalan satu jalur. Baris pertama selesai dulu, baru baris berikutnya jalan. Kali ini kita belajar hal yang membuat Go terkenal: menjalankan banyak pekerjaan sekaligus. Namanya concurrency.
Bayangkan Antrean di Minimarket
Bayangkan minimarket dengan satu kasir. Ada sepuluh pembeli mengantre. Kasir melayani satu per satu. Pembeli kesepuluh harus menunggu sembilan orang di depannya. Ini yang disebut sequential. Satu pekerjaan selesai, baru pekerjaan berikutnya mulai.
Sekarang bayangkan minimarket itu membuka tiga kasir. Antrean terpecah. Sepuluh pembeli selesai jauh lebih cepat. Ini gambaran concurrent. Beberapa pekerjaan berjalan dalam waktu yang beririsan.
Program kita juga sering menghadapi antrean seperti ini. Ambil data dari database, panggil API pembayaran, kirim email notifikasi. Kalau semua dikerjakan bergiliran, pengguna menunggu lama. Go menyediakan cara yang sangat sederhana untuk membuka kasir tambahan. Inilah salah satu alasan besar kenapa perusahaan seperti Google, Uber, dan Gojek memilih Go untuk backend mereka.
Goroutine: Kasir Tambahan di Programmu
Goroutine adalah function yang berjalan bersamaan dengan function lain. Membuatnya gampang sekali. Cukup tambahkan kata go di depan pemanggilan function.
package main
import "fmt"
func sapa() {
fmt.Println("Halo dari goroutine")
}
func main() {
go sapa()
fmt.Println("Main selesai")
}
Coba jalankan. Kemungkinan besar outputnya cuma satu baris:
Main selesai
Tulisan dari sapa tidak muncul sama sekali. Kenapa?
Function main sendiri sebenarnya juga goroutine. Namanya main goroutine. Saat main selesai, program langsung berhenti. Semua goroutine lain ikut mati, tidak peduli sudah sempat jalan atau belum. Di contoh tadi, go sapa() hanya menitipkan pekerjaan. Main tidak menunggu. Dia lanjut ke baris berikutnya, mencetak “Main selesai”, lalu program tutup. Goroutine sapa belum sempat kebagian giliran.
Kalau pakai analogi kasir: kamu membuka kasir kedua, tapi sebelum kasirnya sempat melayani siapa pun, tokonya sudah kamu tutup.
Menunggu dengan sync.WaitGroup
Kita butuh cara untuk bilang ke main: tunggu dulu, masih ada pekerjaan yang belum selesai. Alat paling umum untuk itu adalah sync.WaitGroup. Cara pakainya tiga langkah:
Add(1)dipanggil setiap kali kamu menitipkan satu pekerjaanDone()dipanggil goroutine saat pekerjaannya selesaiWait()membuat main menunggu sampai hitungannya kembali nol
Biar manfaatnya terasa, kita simulasikan mengunduh tiga data. Setiap unduhan kita anggap butuh dua detik, ditiru dengan time.Sleep. Versi sekuensial dulu.
package main
import (
"fmt"
"time"
)
func unduhData(nama string) {
fmt.Println("Mulai mengunduh", nama)
time.Sleep(2 * time.Second)
fmt.Println("Selesai mengunduh", nama)
}
func main() {
mulai := time.Now()
unduhData("laporan.pdf")
unduhData("foto.zip")
unduhData("video.mp4")
fmt.Println("Total waktu:", time.Since(mulai))
}
Total waktunya sekitar enam detik. Wajar. Tiga pekerjaan, masing-masing dua detik, dikerjakan bergiliran seperti satu kasir melayani tiga pembeli. Sekarang versi concurrent.
package main
import (
"fmt"
"sync"
"time"
)
func unduhData(nama string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Println("Mulai mengunduh", nama)
time.Sleep(2 * time.Second)
fmt.Println("Selesai mengunduh", nama)
}
func main() {
mulai := time.Now()
var wg sync.WaitGroup
daftar := []string{"laporan.pdf", "foto.zip", "video.mp4"}
for _, nama := range daftar {
wg.Add(1)
go unduhData(nama, &wg)
}
wg.Wait()
fmt.Println("Total waktu:", time.Since(mulai))
}
Jalankan dan perhatikan angkanya. Total waktunya sekitar dua detik saja. Tiga unduhan berjalan bersamaan, seperti tiga kasir melayani tiga pembeli sekaligus. Perhatikan juga defer wg.Done(). Dengan defer, Done pasti terpanggil saat function berakhir, apa pun yang terjadi di tengah jalan.
Satu catatan. WaitGroup dikirim sebagai pointer, *sync.WaitGroup. Kamu sudah tahu alasannya dari bagian pointer. Kalau dikirim sebagai salinan, hitungan yang dipegang main tidak akan pernah berkurang, dan Wait() menunggu selamanya.
Channel: Jalur Kirim Data Antar Goroutine
Goroutine di atas hanya mencetak ke layar. Bagaimana kalau goroutine perlu mengirim hasil kerjanya kembali ke main? Jangan tergoda menulis ke variabel bersama. Cara yang aman di Go adalah channel.
Channel itu jalur serah terima data antar goroutine. Satu pihak menyerahkan, pihak lain menerima. Membuatnya pakai make, mengirim pakai panah ke arah channel, menerima pakai panah dari channel.
package main
import "fmt"
func main() {
ch := make(chan string)
go func() {
ch <- "pesan dari goroutine"
}()
pesan := <-ch
fmt.Println(pesan)
}
Ada satu sifat penting yang wajib kamu pahami: channel tanpa buffer bersifat blocking. Pengirim berhenti menunggu sampai ada yang menerima. Penerima juga berhenti menunggu sampai ada yang mengirim.
Bayangkan serah terima barang secara langsung. Kurir dan penerima harus sama-sama hadir. Kalau kurir datang dan rumah kosong, kurir menunggu di depan pintu. Kalau kamu menunggu di rumah dan kurir belum datang, ya kamu yang menunggu. Barang baru berpindah saat keduanya bertemu.
Sifat blocking ini pula yang membuat contoh di atas jalan tanpa WaitGroup. Baris pesan := <-ch menahan main sampai goroutine mengirim sesuatu.
Pola Worker: Hasil Kerja Mengalir Lewat Channel
Sekarang kita gabungkan. Satu goroutine bertugas memproses daftar pekerjaan, hasilnya dikirim satu per satu lewat channel. Main tinggal duduk manis menerima.
package main
import (
"fmt"
"strings"
)
func worker(tugas []string, hasil chan string) {
for _, t := range tugas {
hasil <- strings.ToUpper(t)
}
close(hasil)
}
func main() {
tugas := []string{"laporan", "invoice", "surat jalan"}
hasil := make(chan string)
go worker(tugas, hasil)
for h := range hasil {
fmt.Println("Selesai diproses:", h)
}
}
Ada dua hal baru di sini:
close(hasil)menandakan pengiriman sudah selesai. Menutup channel adalah tugas pengirim, bukan penerima.for h := range hasilterus menerima dari channel sampai channel ditutup. Kalau lupaclose, loop ini menunggu selamanya dan program berakhir deadlock.
Buffered Channel, Sekilas Saja
Channel biasa tidak punya ruang tunggu. Buffered channel punya. Kamu tentukan kapasitasnya saat membuat.
package main
import "fmt"
func main() {
ch := make(chan string, 2)
ch <- "pesanan pertama"
ch <- "pesanan kedua"
fmt.Println(<-ch)
fmt.Println(<-ch)
}
Dengan kapasitas dua, pengirim bisa menaruh dua pesan tanpa menunggu penerima hadir. Pengirim baru terblokir saat buffernya penuh. Ini seperti loker paket di depan rumah. Kurir bisa titip barang lalu pergi, selama lokernya belum penuh.
Buffered channel berguna saat pengirim dan penerima bekerja dengan tempo berbeda, misalnya satu goroutine memproduksi data cepat dan penerimanya mencerna lebih lambat. Untuk sekarang, pakai channel tanpa buffer dulu sampai kamu menemukan alasan yang jelas.
select: Menunggu Beberapa Channel Sekaligus
select mirip switch, tapi khusus untuk channel. Dia menunggu, lalu memilih cabang yang channelnya lebih dulu siap. Pemakaian paling umum untuk pemula adalah timeout: kita hanya mau menunggu sampai batas waktu tertentu.
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
go func() {
time.Sleep(3 * time.Second)
ch <- "data siap"
}()
select {
case pesan := <-ch:
fmt.Println(pesan)
case <-time.After(2 * time.Second):
fmt.Println("Kelamaan, kita batalkan saja")
}
}
Goroutine butuh tiga detik, batas sabar kita dua detik. Jadi cabang time.After yang menang. time.After sendiri mengembalikan channel yang mengirim sinyal setelah durasi tertentu, cocok dipasangkan dengan select. Pola select masih banyak variasinya. Detailnya kita simpan untuk bagian lanjutan seri ini.
Peringatan: Jangan Tabur Goroutine di Semua Tempat
Goroutine memang ringan. Satu program Go bisa menjalankan ribuan goroutine tanpa masalah. Tapi bukan berarti semua kode harus dibuat concurrent. Kode sekuensial lebih mudah dibaca, lebih mudah dicari salahnya. Pakai goroutine saat memang ada yang layak dikerjakan bersamaan, misalnya beberapa panggilan jaringan yang saling bebas.
Bahaya paling umum di dunia concurrency namanya race condition. Ini terjadi saat dua goroutine atau lebih menulis ke variabel yang sama pada waktu beririsan. Hasilnya tidak bisa ditebak.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
counter := 0
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++
}()
}
wg.Wait()
fmt.Println("Counter:", counter)
}
Logikanya counter harus 1000. Coba jalankan beberapa kali. Angkanya sering meleset. Penyebabnya, counter++ sebenarnya tiga langkah: baca nilai, tambah satu, tulis balik. Saat dua goroutine melakukannya bersamaan, langkah mereka bisa saling menimpa.
Solusinya ada dua jalur: mengunci akses dengan mutex, atau mengubah desain supaya data mengalir lewat channel seperti contoh worker tadi. Keduanya akan kita bedah di kesempatan lain. Yang perlu kamu ingat sekarang, Go punya detektor bawaan. Jalankan programmu dengan go run -race main.go, dan Go akan melapor kalau menemukan race condition.
Latihan: Cek Harga dari Tiga Supplier
Saatnya latihan utuh. Skenarionya begini. Aplikasi gudangmu perlu membandingkan harga satu barang dari tiga supplier. Setiap supplier punya waktu respons berbeda, kita tiru dengan delay. Kalau dicek satu per satu, total waktunya adalah jumlah semua delay. Kita kerjakan secara concurrent, kumpulkan hasilnya lewat channel, lalu cari penawaran termurah.
package main
import (
"fmt"
"time"
)
type Penawaran struct {
Supplier string
Harga int
}
func cekHarga(supplier string, harga int, delay time.Duration, hasil chan Penawaran) {
time.Sleep(delay)
hasil <- Penawaran{Supplier: supplier, Harga: harga}
}
func main() {
mulai := time.Now()
hasil := make(chan Penawaran)
go cekHarga("Supplier A", 152000, 2*time.Second, hasil)
go cekHarga("Supplier B", 148500, 1*time.Second, hasil)
go cekHarga("Supplier C", 150000, 3*time.Second, hasil)
var termurah Penawaran
for i := 0; i < 3; i++ {
p := <-hasil
fmt.Printf("%s menawarkan Rp%d\n", p.Supplier, p.Harga)
if termurah.Harga == 0 || p.Harga < termurah.Harga {
termurah = p
}
}
fmt.Printf("Termurah: %s dengan harga Rp%d\n", termurah.Supplier, termurah.Harga)
fmt.Println("Total waktu:", time.Since(mulai))
}
Kalau dicek sekuensial, total delaynya enam detik. Versi concurrent ini selesai sekitar tiga detik, mengikuti supplier yang paling lambat. Tiga kasir bekerja, toko tutup begitu pembeli terakhir selesai.
Perhatikan juga urutan hasilnya. Supplier B muncul paling dulu karena delaynya paling pendek. Hasil dari channel datang sesuai siapa yang selesai duluan, bukan sesuai urutan pemanggilan. Kita menerima tepat tiga kali lewat loop, jadi tidak perlu close di sini karena jumlah datanya sudah pasti.
Untuk latihan mandiri: tambahkan supplier keempat, lalu coba pasang timeout dengan select supaya supplier yang jawabnya lebih dari dua detik dianggap gugur.
Rangkuman dan Lanjut ke Mana
Hari ini kamu sudah memegang dasar concurrency di Go:
- Kata
gomenjalankan function sebagai goroutine - Main yang selesai duluan mematikan semua goroutine, jadi kita menunggu dengan
sync.WaitGroup - Channel adalah jalur serah terima data yang aman, sifat dasarnya blocking
closedanrangedipakai saat jumlah kiriman tidak pasti- Buffered channel memberi ruang tunggu, select memilih channel yang siap duluan
- Race condition nyata, dan
go run -racemembantu menangkapnya
Materi ini akan langsung terpakai. Di bagian berikutnya, Belajar Golang dari Nol #11: Membuat REST API Pertamamu dengan net/http, kamu akan lihat bahwa setiap request yang masuk ke server Go dilayani oleh goroutine sendiri. Konsep hari ini adalah pondasinya.
Kalau ada yang terlewat, ulangi dulu bagian 9 tentang interface sebelum lanjut. Dan kalau bisnismu butuh sistem aplikasi yang cepat dan andal seperti yang biasa dibangun dengan Go, tim Arrazy siap membantu.
Artikel Lainnya di Kategori Golang
Golang 3 Agustus 2026
Belajar Golang dari Nol #8: Package dan Go Module
Lanjutan seri Go pemula: pecah kode ke banyak file dengan package, pahami go.mod, aturan huruf besar exported, sampai import package pihak ketiga.
Baca Artikel
Golang 6 Agustus 2026
Belajar Golang dari Nol #11: REST API Pertamamu dengan net/http
Seri Go pemula sampai di gerbang backend: bikin REST API dengan net/http, routing method Go 1.22, JSON response, dan API produk mini lengkap.
Baca Artikel
Golang 28 Juli 2026
Belajar Golang dari Nol #2: Variabel, Tipe Data, dan Zero Value
Lanjutan seri Go pemula: tiga cara deklarasi variabel, tipe data dasar, zero value, konstanta, konversi tipe, plus latihan program hitung belanja.
Baca ArtikelIngin Membaca Artikel Lainnya?
Temukan lebih banyak insight dan tips tentang teknologi dan bisnis digital.
Lihat Semua Artikel