Go Concurrency: Bukan Sekadar Goroutines
Halo, Cecep Azhar di sini. Salah satu alasan utama saya memilih Go untuk production systems adalah concurrency model-nya yang excellent. Goroutines ringan, channels aman, dan pattern-nya sudah mature. Tapi banyak developer yang hanya tahu permukaan — mereka tahu "pakai go keyword" tapi tidak memahami patterns yang tepat.
Mari kita bahas concurrency patterns yang benar-benar berguna di production.
1. Fan-Out Fan-In Pattern
Pattern ini saya gunakan saat butuh memproses banyak data secara paralel. Saya pernah pakai ini untuk memproses 10,000+ records dalam beberapa detik yang seharusnya butuh 10+ menit secara serial.
func processBatch(items []Item) []Result {
inputCh := make(chan Item, len(items))
resultCh := make(chan Result, len(items))
numWorkers := runtime.NumCPU()
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for item := range inputCh {
result := processItem(item)
resultCh <- result
}
}()
}
for _, item := range items {
inputCh <- item
}
close(inputCh)
go func() {
wg.Wait()
close(resultCh)
}()
var results []Result
for result := range resultCh {
results = append(results, result)
}
return results
}
2. Worker Pool Pattern
Ini pattern paling sering saya gunakan di production. Controlled concurrency — tidak spawn goroutine tanpa batas:
type WorkerPool struct {
jobs chan Job
results chan Result
workers int
}
func NewWorkerPool(workers int) *WorkerPool {
return &WorkerPool{
jobs: make(chan Job, 100),
results: make(chan Result, 100),
workers: workers,
}
}
func (wp *WorkerPool) Start(ctx context.Context) {
var wg sync.WaitGroup
for i := 0; i < wp.workers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case job, ok := <-wp.jobs:
if !ok {
return
}
result := process(ctx, job)
wp.results <- result
}
}
}(i)
}
go func() {
wg.Wait()
close(wp.results)
}()
}
3. Pipeline Pattern
Untuk memproses data melalui beberapa tahap secara paralel. Setiap stage berjalan di goroutine terpisah dan terhubung melalui channels:
func pipeline(ctx context.Context, input <-chan Record) <-chan error {
validated := fetch(ctx, input)
transformed := validate(ctx, validated)
stored := transform(ctx, transformed)
return store(ctx, stored)
}
Error Handling di Concurrency
Ini bagian yang paling sering salah di production. Dari pengalaman saya:
- Gunakan context cancellation untuk stop semua goroutines saat error kritis terjadi
- Jangan panic di goroutine — tangkap dengan recover dan log error
- Gunakan errgroup untuk patterns yang butuh error propagation
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return fetchAndProcess(ctx, data)
})
g.Go(func() error {
return syncExternal(ctx, data)
})
if err := g.Wait(); err != nil {
log.Error("Pipeline failed", "error", err)
return err
}
Kesimpulan
Concurrency di Go itu powerful tapi harus hati-hati. Selalu pakai context untuk cancellation, jangan spawn goroutine tanpa batas, dan test dengan race detector (go test -race). Pattern-pattern di atas sudah saya buktikan di production — gunakan sesuai kebutuhan Anda.