# 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. - Published: 2026-02-21 - Updated: 2026-04-29 - Author: SharpSkill - Tags: go, golang, concurrency, goroutines, channels - Reading time: 14 min --- 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. > **Filosofi Go** > > "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. ```go // goroutines_basic.go 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. ```go // channels_basic.go 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. ```go // buffered_channels.go 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. > **Hati-hati Deadlock** > > 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. ```go // select_example.go 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. ## 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. ```go // worker_pool.go 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. ```go // fan_out_fan_in.go 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. ```go // context_example.go 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) } } ``` > **Praktik Terbaik** > > 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. ```go // mutex_example.go 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. ```go // common_mistakes.go 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 ./...`. ## 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. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/id/blog/go/go-concurrency-goroutines-channels