Langsung ke konten
KamusNgoding
Menengah Go 4 menit baca

Memahami Goroutine dan Channel untuk Konkurensi di Go

#go #golang #concurrency #goroutine #channel

Memahami Goroutine dan Channel untuk Konkurensi di Go

Pendahuluan

Bayangkan kamu sedang membangun backend untuk aplikasi ride-hailing seperti seperti Gojek — ratusan ribu permintaan masuk setiap detik: cek posisi driv driver, kalkulasi tarif, kirim notifikasi, simpan log. Jika setiap perminta permintaan harus menunggu yang sebelumnya selesai, sistemmu akan kolaps dal dalam hitungan menit.

Di sinilah konkurensi menjadi kunci. Go dirancang sejak awal dengan kon konkurensi sebagai fitur inti, bukan tambahan. Dua mekanisme utamanya — **g goroutine dan channel — memungkinkan kamu menulis kode konkuren yan yang bersih, efisien, dan mudah dibaca.

Artikel ini mengasumsikan kamu sudah familiar dengan sintaks dasar Go. Kita 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 paralelisme). Konkurensi adalah kemampuan program untuk mengelola banyak  pekerjaan sekaligus — meskipun di satu waktu hanya satu yang berjalan.

Go menerapkan model konkurensi berbasis CSP (Communicating Sequential Pro Processes). Prinsipnya sederhana:

“Jangan berkomunikasi dengan berbagi memori; sebaliknya, bagikan memori  dengan berkomunikasi.”

Ini berbeda dari pendekatan thread tradisional di Java atau C++ yang mengan mengandalkan mutex dan shared memory. Kalau kamu terbiasa dengan konsep sep seperti [Inheritance dan Polymorphism di C++](/docs/sw/cpp/inheritance-dan-K C++ yang fokus pada struktu struktur objek, cara berpikir Go ini membutuhkan sedikit pergeseran mental. mental.


Goroutine: Konkurensi Ringan di Go

Goroutine adalah fungsi yang berjalan secara konkuren dengan fungsi lai 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
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 
Go
# dapat mengeksekusinya secara bergantian.
*/

Output akan menampilkan pesan dari kedua goroutine yang bercampur — itulah  konkurensi bekerja.

Goroutine vs Thread OS

AspekThread OSGoroutine
Ukuran stack awal~2 MB~2 KB (tumbuh dinamis)
Biaya pembuatanMahalSangat murah
Jumlah praktisRibuanJutaan
PenjadwalanOSGo runtime (M:N scheduling)

Go runtime memetakan banyak goroutine ke sejumlah kecil thread OS. Inilah k kenapa kamu bisa spawn jutaan goroutine tanpa kehabisan memori — sesuatu ya yang tidak mungkin dengan thread konvensional.

Masalah dengan time.Sleep

Menggunakan time.Sleep untuk menunggu goroutine adalah praktik buruk di p 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
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.
concurrent.
*/

Channel: Komunikasi Aman Antar Goroutine

Channel adalah “pipa” yang memungkinkan goroutine saling mengirim dan m menerima nilai secara aman. Channel di Go bersifat type-safe — channel i inthanya bisa membawa nilaiint`.

package main

import "fmt"

func main() {
	unbuffered := make(chan int)    // Channel tanpa buffer: pengirim dan pener
penerima harus siap di saat yang sama
	buffered := make(chan int, 5)   // Channel dengan buffer: bisa menampung sa
sampai 5 nilai

	go func() {
		unbuffered <- 10 // Kirim nilai ke channel unbuffered dari goroutine terpis
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 sebaliknya. Ini seperti serah-terima langsung — kedua pihak harus hadir ber 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 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
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
*/
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
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 dikirim via channel, lalu diproses goroutine lain.

package main

import "fmt"

// generate mengirim setiap angka ke channel, lalu menutup channel saat sel
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
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 Array dan Operasi Dasar dalam Struktur Data](/docs/cs/data-structures/menguK Data —  hanya saja di sini data mengalir secara konkuren melalui beberapa tahap pem pemrosesan.


Pola Konkurensi Umum dengan Select

select memungkinkan goroutine menunggu di beberapa channel sekaligus — me 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.
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  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
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 atau Shopee, kamu perlu memproses ribuan pesanan masuk secara efisien. **Wo Worker pool adalah pola klasik untuk ini: sejumlah worker goroutine sia 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
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
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 bekerja bersama dengan cara yang elegan:

  • Goroutine menyediakan unit eksekusi konkuren yang sangat ringan — kam kamu bisa membuat ribuan tanpa khawatir overhead.
  • Channel menyediakan cara aman dan eksplisit untuk goroutine berkomuni berkomunikasi, menghilangkan kebutuhan akan lock yang rumit.
  • Select memungkinkan penanganan beberapa channel sekaligus, termasuk p pola timeout yang krusial di production.
  • Worker pool adalah pola fundamental yang menggabungkan keduanya untuk untuk pemrosesan beban tinggi yang efisien.

Filosofi Go — “bagikan memori dengan berkomunikasi” — membuat kode konkuren konkuren lebih mudah ditulis dan didebug dibanding pendekatan thread tradis tradisional. Mulai dari pola sederhana seperti contoh di atas, lalu tingkat tingkatkan ke pola yang lebih kompleks sesuai kebutuhan sistemmu.

Artikel Terkait