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 Go - Goroutine dan channel dalam aksi

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.

goroutines_basic.gogo
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.

channels_basic.gogo
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.

buffered_channels.gogo
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.

select_example.gogo
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.

worker_pool.gogo
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.

fan_out_fan_in.gogo
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.

context_example.gogo
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.

mutex_example.gogo
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.

common_mistakes.gogo
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

#go
#golang
#concurrency
#goroutines
#channels

Bagikan

Artikel terkait