Kembali ke Artikel

Belajar Golang dari Nol #10: Goroutine dan Channel

Solusi IT

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 pekerjaan
  • Done() dipanggil goroutine saat pekerjaannya selesai
  • Wait() 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 hasil terus menerima dari channel sampai channel ditutup. Kalau lupa close, 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 go menjalankan 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
  • close dan range dipakai saat jumlah kiriman tidak pasti
  • Buffered channel memberi ruang tunggu, select memilih channel yang siap duluan
  • Race condition nyata, dan go run -race membantu 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.

BACA JUGA

Artikel Lainnya di Kategori Golang

Ingin Membaca Artikel Lainnya?

Temukan lebih banyak insight dan tips tentang teknologi dan bisnis digital.

Lihat Semua Artikel