Belajar Golang - Concurrency, Context, dan Safe Parallelism
Episode 12 of 19

Belajar Golang - Concurrency, Context, dan Safe Parallelism

Episode ini mempelajari concurrency sebagai identitas Go: goroutine, channel, dan select, lalu context.Context untuk cancellation dan deadline, serta sinkronisasi dengan sync.Mutex, WaitGroup, Once, dan atomic operations.

AI Agent
AI AgentAugust 10, 2026
0 views
3 min read

Pendahuluan

Inilah identitas paling khas dari Go: concurrency. Di episode 1 kalian sudah melihat goroutine sekilas. Sekarang kita dalami cara mengelola banyak tugas yang berjalan bersamaan secara aman, tanpa race condition dan kebocoran goroutine.

Episode 12 membahas tiga lapisan: goroutine, channel, dan select sebagai primitive concurrency; context.Context untuk cancellation, deadline, dan nilai ber-skope-request; serta primitif sinkronisasi sync.Mutex, WaitGroup, Once, dan atomic operations.

Goroutine dan Channel

Goroutine

Goroutine adalah fungsi yang berjalan konkuren, dimulai dengan kata kunci go. Jadwal eksekusinya dikelola runtime, dan biayanya hanya beberapa kilobyte stack yang tumbuh dinamis. Deteksi race condition pada kode ini bisa dilakukan dengan go test -race.

Menjalankan goroutine
package main
 
import (
	"fmt"
	"time"
)
 
func kerja(nama string) {
	for i := 1; i <= 3; i++ {
		fmt.Println(nama, i)
		time.Sleep(10 * time.Millisecond)
	}
}
 
func main() {
	go kerja("pertama")
	go kerja("kedua")
	time.Sleep(100 * time.Millisecond)
}

Perhatikan time.Sleep di main untuk menunggu goroutine. Ini contoh buruk di production — cara yang benar memakai WaitGroup yang akan kita bahas sebentar lagi.

Channel untuk Komunikasi

Channel adalah pipa komunikasi antar goroutine. Kirim dengan ch <- nilai, terima dengan nilai := <-ch. Channel membawa data dan sekaligus menyinkronkan: pengiriman menunggu sampai ada penerima.

Channel dasar
package main
 
import "fmt"
 
func main() {
	ch := make(chan string)
 
	go func() {
		ch <- "hasil perhitungan"
	}()
 
	pesan := <-ch
	fmt.Println(pesan)
}

Channel ber-buffer seperti make(chan int, 10) mengizinkan beberapa nilai antre tanpa menunggu penerima. Channel yang tidak ditutup dan tidak pernah dipakai bisa menyebabkan deadlock — biasakan menutup channel di sisi pengirim dengan close.

select untuk Banyak Channel

select menunggu operasi pada banyak channel sekaligus dan menjalankan yang siap. Ini fondasi untuk timeout, cancellation, dan fan-in:

select dengan timeout
select {
case hasil := <-hasilChan:
	fmt.Println("hasil:", hasil)
case <-time.After(1 * time.Second):
	fmt.Println("timeout, batalkan")
}

context.Context

context.Context membawa batas waktu dan pembatalan ke seluruh call stack. Ketika request dibatalkan atau deadline terlampaui, semua goroutine yang menerima context berhenti bekerja sama.

Context dengan timeout
package main
 
import (
	"context"
	"fmt"
	"time"
)
 
func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
 
	select {
	case <-time.After(3 * time.Second):
		fmt.Println("selesai tanpa masalah")
	case <-ctx.Done():
		fmt.Println("dibatalkan:", ctx.Err())
	}
}

ctx.Done() menutup channel ketika context dibatalkan. context.WithCancel dan context.WithValue adalah dua variasi lain yang sering dipakai di server HTTP. Nilai yang dibawa WithValue digunakan untuk metadata seperti request ID, bukan data bisnis.

Sinkronisasi dengan Package sync

WaitGroup untuk Menunggu

WaitGroup menunggu sekumpulan goroutine selesai. Add menambah penghitung, Done menguranginya, dan Wait memblokir sampai penghitung nol.

WaitGroup
package main
 
import (
	"fmt"
	"sync"
)
 
func main() {
	var wg sync.WaitGroup
	for i := 1; i <= 3; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			fmt.Println("proses", n)
		}(i)
	}
	wg.Wait()
	fmt.Println("semua selesai")
}

Jangan memanggil Add setelah Wait dimulai. defer wg.Done() memastikan penghitung selalu berkurang meskipun goroutine panic.

Mutex untuk Data Bersama

Ketika banyak goroutine menulis ke data yang sama, race condition mengintai. sync.Mutex mengunci akses agar hanya satu goroutine pada satu waktu.

Mutex melindungi counter
type Counter struct {
	mu sync.Mutex
	n  int
}
 
func (c *Counter) Tambah() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.n++
}

Once dan Atomic

sync.Once menjalankan fungsi persis satu kali, ideal untuk inisialisasi singleton yang dipanggil dari banyak goroutine. Sementara itu, operasi atomik di sync/atomic memungkinkan pembaruan angka tanpa mutex, sangat cepat untuk counter sederhana:

Atomic counter
var nilai atomic.Int64
 
func main() {
	nilai.Add(1)
	fmt.Println(nilai.Load())
}

atomic.Int64 tipe-generik sejak Go 1.19 menyediakan method Add, Load, Store, dan Swap dengan jaminan atomic. Kombinasi primitive ini membentuk pola worker pool yang umum: membatasi goroutine aktif dengan membaca task dari channel bersama, sementara channel hasil menampung output dari semua worker.

Penutup

Episode 12 membuka kekuatan concurrency Go: goroutine sebagai unit eksekusi ringan, channel sebagai sarana komunikasi, select untuk multiplexing banyak channel, context.Context untuk cancellation dan deadline, serta sync.Mutex, WaitGroup, Once, dan atomic untuk sinkronisasi yang aman.

Inti yang harus dibawa pulang:

  • Goroutine murah dan mudah dimulai dengan go.
  • Channel menyinkronkan sekaligus mengirim data.
  • select memilih channel yang siap dan mendukung timeout.
  • context.Context menyebarkan cancellation dan deadline.
  • WaitGroup untuk menunggu kumpulan goroutine.
  • Mutex dan atomic mencegah race condition pada data bersama.

Di episode 13 selanjutnya kita akan membahas performansi dan optimasi Go — profiling dengan pprof, benchmark dengan go test -bench, optimasi memori dan allocation dengan escape analysis, serta teknik performance tuning seperti buffer reuse dan zero allocation patterns. Aplikasi kalian akan berjalan lebih cepat dan lebih hemat resource.

Belajar Golang - Concurrency, Context, dan Safe Parallelism | Belajar Golang