BAB 23: Channel — Komunikasi Antar Goroutine

Pelajari cara goroutine bertukar data secara aman menggunakan channel, mekanisme komunikasi bawaan Go yang thread-safe.

Di bab sebelumnya, kamu sudah menguasai goroutine — cara meluncurkan pekerjaan yang berjalan concurrent dan menggunakan sync.WaitGroup untuk menunggu semuanya tuntas. Tapi ada satu hal yang belum tersentuh: bagaimana goroutine saling berbicara? Goroutine yang berjalan sendiri tanpa cara untuk berkomunikasi seperti pekerja di gudang yang tidak punya cara untuk memberitahu satu sama lain bahwa tugasnya sudah selesai atau barang sudah siap diambil.

Channel adalah mekanisme komunikasi itu — pipa yang menghubungkan goroutine satu dengan goroutine lainnya, sekaligus berfungsi sebagai alat sinkronisasi yang lebih ekspresif dari WaitGroup.

Membuat dan Menggunakan Channel

Channel dibuat dengan make dan keyword chan diikuti tipe data yang akan ditransfer.

// main.go
package main

import "fmt"

func kirimStatus(ch chan string, pesan string) {
    ch <- pesan // kirim data ke channel
}

func main() {
    statusCh := make(chan string)

    go kirimStatus(statusCh, "laporan Q1 selesai diproses")

    hasil := <-statusCh // terima data dari channel
    fmt.Println("Status:", hasil)
}
Status: laporan Q1 selesai diproses

Kali ini program menunggu. Operator <- bekerja dua arah: ch <- data berarti kirim ke channel, sedangkan data := <-ch berarti terima dari channel. Keduanya bersifat blocking — pengirim menunggu ada penerima, dan penerima menunggu ada data yang dikirim. Inilah yang membuat main tidak keluar lebih dulu.

Channel Sebagai Parameter Fungsi

Channel adalah nilai yang dikirim sebagai referensi. Kamu bisa meneruskannya ke fungsi lain seperti parameter biasa, dan fungsi itu akan bekerja pada channel yang sama.

// main.go
package main

import "fmt"

func hitungTotal(harga []int, hasilCh chan int) {
    total := 0
    for _, h := range harga {
        total += h
    }
    hasilCh <- total
}

func main() {
    daftarHarga := []int{15000, 32000, 8500, 45000, 12000}
    hasilCh := make(chan int)

    go hitungTotal(daftarHarga, hasilCh)

    total := <-hasilCh
    fmt.Printf("Total transaksi: Rp%d\n", total)
}
Total transaksi: Rp112500

hitungTotal berjalan sebagai goroutine, melakukan kalkulasi, lalu mengirim hasilnya ke hasilCh. main menunggu di <-hasilCh sampai hasil tersedia. Tidak perlu time.Sleep atau mekanisme tunggu lain — channel sudah mengurus sinkronisasinya.

Menerima dari Beberapa Goroutine

Channel yang sama bisa dipakai untuk menerima dari banyak goroutine. Urutannya tidak dijamin — goroutine mana yang selesai lebih dulu, itulah yang datanya masuk ke channel pertama.

// main.go
package main

import "fmt"

type HasilKerja struct {
    ID    int
    Nilai string
}

func prosesItem(id int, teks string, ch chan HasilKerja) {
    hasil := HasilKerja{ID: id, Nilai: teks + " [selesai]"}
    ch <- hasil
}

func main() {
    tugasCh := make(chan HasilKerja)

    items := []string{"inventaris", "keuangan", "sdm"}

    for i, item := range items {
        go prosesItem(i+1, item, tugasCh)
    }

    for range items {
        h := <-tugasCh
        fmt.Printf("Laporan #%d: %s\n", h.ID, h.Nilai)
    }
}
Laporan #2: keuangan [selesai]
Laporan #1: inventaris [selesai]
Laporan #3: sdm [selesai]

Urutan output bisa berbeda setiap kali dijalankan — dan memang begitu seharusnya. Yang penting, semua tiga hasil pasti diterima karena loop for range items akan menunggu tepat tiga kali di <-tugasCh.

Urutan penerimaan data dari channel mencerminkan urutan goroutine yang selesai terlebih dahulu, bukan urutan goroutine saat diluncurkan. Ini adalah karakteristik fundamental dari concurrent programming.

Buffered vs Unbuffered Channel

Semua channel yang kita buat sejauh ini adalah unbuffered — setiap pengiriman akan memblokir sampai ada penerima yang siap, dan sebaliknya. Ini adalah mode sinkron yang ketat.

Go juga mendukung buffered channel — channel dengan kapasitas penyimpanan sementara. Pengiriman ke buffered channel tidak memblokir selama masih ada ruang di buffer.

// main.go
package main

import "fmt"

func main() {
    // buffered channel dengan kapasitas 3
    antrianCh := make(chan string, 3)

    // bisa kirim 3 data tanpa ada receiver
    antrianCh <- "laporan-01"
    antrianCh <- "laporan-02"
    antrianCh <- "laporan-03"

    fmt.Println("tiga laporan masuk antrian")

    // terima satu per satu
    fmt.Println(<-antrianCh)
    fmt.Println(<-antrianCh)
    fmt.Println(<-antrianCh)
}
tiga laporan masuk antrian
laporan-01
laporan-02
laporan-03

Berbeda dengan unbuffered, tiga operasi kirim di atas tidak perlu goroutine terpisah karena buffer menampungnya. Data diterima dalam urutan FIFO — pertama masuk, pertama keluar.

Perbandingan antara keduanya:

AspekUnbufferedBuffered
Blocking saat kirimSelalu, sampai ada receiverHanya saat buffer penuh
Blocking saat terimaSelalu, sampai ada senderHanya saat buffer kosong
SinkronisasiKetat, sinkronLebih longgar, asinkron
Kapan digunakanSinkronisasi langsungAntrian, rate limiting

Sender bisa menutup channel dengan close() untuk memberi sinyal bahwa tidak ada data lagi yang akan dikirim. Penerima bisa mendeteksi ini dengan bentuk dua-nilai dari operasi receive.

// main.go
package main

import "fmt"

func generateKode(ch chan int) {
    for i := 1001; i <= 1005; i++ {
        ch <- i
    }
    close(ch) // sinyal: tidak ada data lagi
}

func main() {
    kodeCh := make(chan int)

    go generateKode(kodeCh)

    for kode := range kodeCh {
        fmt.Printf("memproses kode: %d\n", kode)
    }

    fmt.Println("semua kode selesai diproses")
}
memproses kode: 1001
memproses kode: 1002
memproses kode: 1003
memproses kode: 1004
memproses kode: 1005
semua kode selesai diproses

for kode := range kodeCh secara otomatis berhenti saat channel ditutup — tidak perlu menghitung berapa data yang akan diterima. Ini adalah pola yang sangat idiomatik di Go untuk mengirim sejumlah data yang tidak diketahui jumlahnya lebih awal.

Hanya sender yang boleh menutup channel. Menutup channel dari sisi receiver, atau mengirim data ke channel yang sudah ditutup, akan menyebabkan panic saat runtime.

Latihan

Latihan 1 — Paralel dengan hasil: Buat program yang meluncurkan tiga goroutine secara bersamaan. Setiap goroutine menerima sepasang angka dan mengembalikan hasil perkaliannya. Gunakan channel untuk mengumpulkan semua hasil dan cetak totalnya.

Latihan 2 — Antrian dengan buffered channel: Simulasikan antrian nomor tiket menggunakan buffered channel berkapasitas 5. Buat goroutine yang mengisi antrian dengan nomor 101–110, dan goroutine lain yang membaca antrian satu per satu sambil mencetak “melayani tiket #N”. Pastikan semua tiket terlayani sebelum program selesai.

Latihan 3 — Pipeline sederhana: Buat dua fungsi yang masing-masing berjalan sebagai goroutine. Fungsi pertama menghasilkan string nama file (misal "data-01.csv" sampai "data-05.csv") dan mengirimnya ke channel. Fungsi kedua menerima nama file tersebut, menambahkan prefix "processed/", dan mengirimnya ke channel berikutnya. main mencetak hasil akhirnya. Ini adalah pola pipeline — salah satu pola concurrency paling umum di Go.

Channel adalah fondasi dari cara Go menangani concurrency. Dengan memahami bagaimana goroutine berkomunikasi melalui channel — baik unbuffered maupun buffered, satu arah maupun pipeline — kamu sudah memiliki alat untuk membangun program yang bisa memanfaatkan banyak core secara efisien. Ekosistem concurrency Go masih lebih luas dari ini: ada select untuk menunggu multiple channel, sync.WaitGroup untuk koordinasi goroutine yang lebih kompleks, dan context untuk membatalkan goroutine yang tidak lagi dibutuhkan. Tapi semuanya bertumpu pada pemahaman channel yang sudah kamu bangun di bab ini.

Referensi

  1. 1Channel types — The Go Programming Language Specification
  2. 2Channels — A Tour of Go
  3. 3Channels — Go by Example