Konkurensi di Go: Goroutine dan Channel - Panduan Lengkap
Kuasai konkurensi di Go dengan goroutine dan channel. Pola lanjutan, sinkronisasi, pernyataan select, dan praktik terbaik dengan contoh kode terperinci.

Konkurensi merupakan salah satu kekuatan terbesar Go. Berbeda dengan bahasa lain di mana multithreading tetap kompleks, Go menawarkan model elegan berbasis goroutine dan channel yang menyederhanakan secara signifikan pengembangan aplikasi konkuren.
"Jangan berkomunikasi dengan berbagi memori; berbagi memori dengan berkomunikasi." Prinsip mendasar ini memandu seluruh desain konkurensi di Go.
Memahami Goroutine
Goroutine adalah thread ringan yang dikelola oleh runtime Go. Goroutine mengonsumsi sekitar 2 KB stack (dibandingkan beberapa MB untuk thread sistem operasi) dan memungkinkan menjalankan ribuan tugas konkuren tanpa membebani sistem.
Menjalankan goroutine cukup dengan menempatkan kata kunci go sebelum panggilan fungsi. Runtime menangani penjadwalan dan distribusi di antara thread yang tersedia.
package main
import (
"fmt"
"time"
)
// fetchData simulates a network request
func fetchData(id int) {
// Simulates network delay
time.Sleep(100 * time.Millisecond)
fmt.Printf("Data %d fetched\n", id)
}
func main() {
// Sequential execution - 500ms total
start := time.Now()
for i := 1; i <= 5; i++ {
fetchData(i)
}
fmt.Printf("Sequential: %v\n", time.Since(start))
// Concurrent execution - ~100ms total
start = time.Now()
for i := 1; i <= 5; i++ {
go fetchData(i) // Execute as goroutine
}
time.Sleep(150 * time.Millisecond) // Wait for completion
fmt.Printf("Concurrent: %v\n", time.Since(start))
}Eksekusi konkuren mengurangi total waktu dari 500ms menjadi sekitar 100ms. Namun, menggunakan time.Sleep untuk sinkronisasi goroutine bukan praktik terbaik. Channel menawarkan solusi elegan.
Channel: Komunikasi Antar Goroutine
Sebuah channel adalah saluran bertipe untuk mengirim dan menerima nilai antar goroutine. Channel menjamin sinkronisasi: goroutine pengirim menunggu hingga yang lain menerima, dan sebaliknya.
Pembuatan channel menggunakan fungsi make. Operator <- mengirim dan menerima data tergantung pada posisinya relatif terhadap channel.
package main
import "fmt"
// worker performs computation and returns result via channel
func worker(id int, jobs <-chan int, results chan<- int) {
// Receives jobs until channel closes
for job := range jobs {
result := job * 2 // Processing
results <- result // Send result
}
}
func main() {
// Create channels
jobs := make(chan int, 10) // Buffered channel
results := make(chan int, 10)
// Start 3 workers
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}
// Send 5 jobs
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs) // Signal end of jobs
// Collect results
for r := 1; r <= 5; r++ {
result := <-results
fmt.Printf("Result: %d\n", result)
}
}Channel terarah (<-chan untuk penerimaan, chan<- untuk pengiriman) memperkuat keamanan kode dengan membatasi operasi yang mungkin.
Channel Berbuffer vs Tanpa Buffer
Perbedaan antara jenis channel ini secara langsung memengaruhi perilaku sinkronisasi antar goroutine.
Channel tanpa buffer memblokir pengirim hingga penerima siap. Channel berbuffer memungkinkan mengirim hingga N nilai tanpa memblokir, di mana N mewakili kapasitas buffer.
package main
import "fmt"
func main() {
// Unbuffered channel - strict synchronization
unbuffered := make(chan string)
go func() {
unbuffered <- "message" // Blocks until received
}()
msg := <-unbuffered // Unblocks the send
fmt.Println(msg)
// Buffered channel - capacity of 2
buffered := make(chan int, 2)
// These sends don't block
buffered <- 1
buffered <- 2
// buffered <- 3 // Would block because buffer is full
fmt.Println(<-buffered) // 1
fmt.Println(<-buffered) // 2
// Check capacity
fmt.Printf("Length: %d, Capacity: %d\n",
len(buffered), cap(buffered))
}Channel berbuffer memisahkan produsen dan konsumen, sementara yang tanpa buffer menjamin sinkronisasi titik-ke-titik.
Deadlock terjadi ketika semua goroutine terblokir menunggu. Runtime Go mendeteksi ini dan menghentikan program dengan pesan kesalahan eksplisit.
Select: Multiplexing Channel
Pernyataan select menunggu operasi simultan pada beberapa channel. Bentuknya menyerupai pernyataan switch untuk komunikasi konkuren.
Konstruksi ini esensial untuk mengelola timeout, pembatalan, dan beberapa komunikasi tanpa terblokir tanpa batas pada satu channel.
package main
import (
"fmt"
"time"
)
// fetchAPI simulates an API call with variable delay
func fetchAPI(name string, delay time.Duration, ch chan<- string) {
time.Sleep(delay)
ch <- fmt.Sprintf("%s: data received", name)
}
func main() {
api1 := make(chan string)
api2 := make(chan string)
// Launch two API calls in parallel
go fetchAPI("API-1", 100*time.Millisecond, api1)
go fetchAPI("API-2", 200*time.Millisecond, api2)
// Global timeout of 150ms
timeout := time.After(150 * time.Millisecond)
// Collect results with timeout
for i := 0; i < 2; i++ {
select {
case result := <-api1:
fmt.Println(result)
case result := <-api2:
fmt.Println(result)
case <-timeout:
fmt.Println("Timeout - operation cancelled")
return
}
}
}select memilih channel pertama yang siap. Jika beberapa siap, pilihan bersifat pseudo-acak untuk mencegah starvation.
Siap menguasai wawancara Go Anda?
Berlatih dengan simulator interaktif, flashcards, dan tes teknis kami.
Pola Worker Pool
Pola worker pool mendistribusikan tugas di antara beberapa worker, membatasi konkurensi, dan mengoptimalkan penggunaan sumber daya. Pola ini terbukti sangat penting untuk memproses sejumlah besar data.
Implementasinya bergantung pada channel tugas yang dibagikan di antara worker dan channel hasil untuk pengumpulan.
package main
import (
"fmt"
"sync"
"time"
)
// Task represents a unit of work
type Task struct {
ID int
Data string
}
// Result contains the processing result
type Result struct {
TaskID int
Output string
}
// worker processes received tasks
func worker(id int, tasks <-chan Task, results chan<- Result, wg *sync.WaitGroup) {
defer wg.Done()
for task := range tasks {
// Simulate processing
time.Sleep(50 * time.Millisecond)
results <- Result{
TaskID: task.ID,
Output: fmt.Sprintf("Worker %d processed: %s", id, task.Data),
}
}
}
func main() {
const numWorkers = 3
const numTasks = 10
tasks := make(chan Task, numTasks)
results := make(chan Result, numTasks)
var wg sync.WaitGroup
// Start workers
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, tasks, results, &wg)
}
// Send tasks
for i := 1; i <= numTasks; i++ {
tasks <- Task{ID: i, Data: fmt.Sprintf("task-%d", i)}
}
close(tasks)
// Close results channel after workers finish
go func() {
wg.Wait()
close(results)
}()
// Collect results
for result := range results {
fmt.Printf("Task %d: %s\n", result.TaskID, result.Output)
}
}sync.WaitGroup mengoordinasikan menunggu semua worker selesai sebelum menutup channel hasil.
Pola Fan-Out/Fan-In
Pola ini mendistribusikan pekerjaan di antara beberapa goroutine (fan-out) lalu menggabungkan hasil (fan-in). Pola ini memaksimalkan paralelisme sambil menyederhanakan pengumpulan hasil.
package main
import (
"fmt"
"sync"
)
// generate produces numbers on a channel
func generate(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out)
}()
return out
}
// square computes the square of received numbers
func square(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out
}
// merge combines multiple channels into one (fan-in)
func merge(channels ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
// Output function for each channel
output := func(c <-chan int) {
defer wg.Done()
for n := range c {
out <- n
}
}
// Launch a goroutine per channel
wg.Add(len(channels))
for _, c := range channels {
go output(c)
}
// Close after all goroutines finish
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
// Generate data
numbers := generate(1, 2, 3, 4, 5, 6, 7, 8)
// Fan-out: distribute to 3 workers
sq1 := square(numbers)
sq2 := square(numbers)
sq3 := square(numbers)
// Fan-in: aggregate results
for result := range merge(sq1, sq2, sq3) {
fmt.Println(result)
}
}Pola ini unggul untuk operasi terikat CPU yang dapat didistribusikan dan pipeline pemrosesan data.
Context untuk Pembatalan dan Deadline
Paket context menstandarkan pengelolaan pembatalan, deadline, dan nilai antar goroutine. Setiap goroutine yang berjalan lama harus menerima context sebagai parameter pertama.
package main
import (
"context"
"fmt"
"time"
)
// fetchWithContext simulates a cancellable request
func fetchWithContext(ctx context.Context, url string) (string, error) {
// Simulates a long operation
select {
case <-time.After(2 * time.Second):
return fmt.Sprintf("Data from %s", url), nil
case <-ctx.Done():
return "", ctx.Err() // context.Canceled or context.DeadlineExceeded
}
}
func main() {
// Context with 500ms timeout
ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel() // Release resources
result, err := fetchWithContext(ctx, "https://api.example.com")
if err != nil {
fmt.Printf("Error: %v\n", err)
return
}
fmt.Println(result)
// Context with manual cancellation
ctx2, cancel2 := context.WithCancel(context.Background())
go func() {
time.Sleep(100 * time.Millisecond)
cancel2() // Explicit cancellation
}()
result, err = fetchWithContext(ctx2, "https://api2.example.com")
if err != nil {
fmt.Printf("Request cancelled: %v\n", err)
}
}Selalu panggil defer cancel() segera setelah membuat context untuk menghindari kebocoran sumber daya.
Sinkronisasi dengan sync.Mutex
Meskipun channel lebih disukai untuk komunikasi, paket sync tetap diperlukan untuk melindungi akses konkuren ke struktur data bersama.
package main
import (
"fmt"
"sync"
)
// SafeCounter is a thread-safe counter
type SafeCounter struct {
mu sync.Mutex
value map[string]int
}
// Increment increments the value for a given key
func (c *SafeCounter) Increment(key string) {
c.mu.Lock() // Exclusive lock
defer c.mu.Unlock() // Guaranteed unlock
c.value[key]++
}
// Value returns the current value
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
defer c.mu.Unlock()
return c.value[key]
}
func main() {
counter := SafeCounter{value: make(map[string]int)}
var wg sync.WaitGroup
// 1000 concurrent increments
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment("visits")
}()
}
wg.Wait()
fmt.Printf("Total: %d\n", counter.Value("visits")) // 1000
}sync.RWMutex mengoptimalkan pembacaan konkuren dengan RLock()/RUnlock() untuk operasi baca saja.
Kesalahan Umum dan Solusinya
Konkurensi di Go memiliki jebakan klasik. Berikut kesalahan paling umum dan cara menghindarinya.
package main
import (
"fmt"
"sync"
)
func main() {
// ERROR: Loop variable capture
// All goroutines would print the same value
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i) // Capture by reference - BUG
}()
}
// SOLUTION: Pass value as parameter
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
fmt.Println(n) // Local copy - CORRECT
}(i)
}
wg.Wait()
// ERROR: Send on nil channel
var ch chan int
// ch <- 1 // Blocks forever
// SOLUTION: Always initialize with make
ch = make(chan int, 1)
ch <- 1
fmt.Println(<-ch)
// ERROR: Send on closed channel
done := make(chan bool)
close(done)
// done <- true // Panic!
// SOLUTION: Check before send or use sync.Once
select {
case done <- true:
fmt.Println("Sent")
default:
fmt.Println("Channel closed or full")
}
}Deteksi race condition menggunakan flag -race selama kompilasi atau pengujian: go test -race ./....
Mulai berlatih!
Uji pengetahuan Anda dengan simulator wawancara dan tes teknis kami.
Kesimpulan
Menguasai konkurensi di Go bergantung pada beberapa konsep kunci yang, ketika dipahami dengan baik, memungkinkan membangun aplikasi berkinerja tinggi.
Poin utama:
✅ Goroutine ringan dan murah - membuat ribuan tetap dapat diterima
✅ Channel menyinkronkan dan mentransfer data antar goroutine
✅ Pernyataan select mengelola beberapa komunikasi dan timeout
✅ Pola worker pool membatasi konkurensi dan mengoptimalkan sumber daya
✅ Paket context menstandarkan pembatalan dan deadline
✅ Mutex melindungi data bersama saat channel tidak mencukupi
✅ Flag -race mendeteksi race condition selama pengujian
Filosofi "Berbagi memori dengan berkomunikasi" mengarah pada desain yang lebih aman dan mudah dikelola dibandingkan multithreading tradisional dengan kunci.
Tag
Bagikan
Artikel terkait

Wawancara Teknis Go: Goroutine, Channel, dan Concurrency
Pertanyaan wawancara teknis Go tentang goroutine, channel, dan pola concurrency. Contoh kode, jebakan umum, dan jawaban tingkat ahli untuk persiapan wawancara teknis Go pada tahun 2026.

25 pertanyaan wawancara Go teratas: panduan lengkap developer
Kuasai wawancara Go dengan 25 pertanyaan paling sering ditanyakan. Goroutine, channel, interface, dan pola konkurensi dengan contoh kode.

Go: Dasar-Dasar untuk Developer Java/Python di 2026
Pelajari Go dengan cepat menggunakan pengalaman Java atau Python. Goroutine, channel, interface, dan pola-pola penting untuk transisi yang lancar.