Concurrency adalah Keunggulan Go
Salah satu alasan utama saya memilih Go untuk backend development adalah concurrency model-nya yang elegant. Goroutines dan channels memudahkan kita menulis kode yang concurrent tanpa complexity seperti threading di bahasa lain. Tapi banyak developer Indonesia yang belum memanfaatkan concurrency Go secara optimal.
Di artikel ini, saya akan share beberapa patterns yang saya gunakan di production system. Semua sudah terbukti reliable di environment concurrent yang tinggi.
Pattern 1: Fan-Out Fan-In
Pattern ini sangat berguna saat kamu perlu memproses banyak data secara parallel dan menggabungkan hasilnya:
func FanOutFanIn(ctx context.Context, items []Item, processFn func(Item) (Result, error)) ([]Result, error) {
// Fan-Out: distribusi work ke banyak goroutines
const numWorkers = 10
itemChan := make(chan Item, len(items))
resultChan := make(chan Result, len(items))
errChan := make(chan error, numWorkers)
// Start workers
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for item := range itemChan {
result, err := processFn(item)
if err != nil {
errChan <- err
continue
}
resultChan <- result
}
}()
}
// Send items to workers
go func() {
for _, item := range items {
itemChan <- item
}
close(itemChan)
}()
// Fan-In: collect results
go func() {
wg.Wait()
close(resultChan)
close(errChan)
}()
var results []Result
var errs []error
for result := range resultChan {
results = append(results, result)
}
for err := range errChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return results, fmt.Errorf("%d errors occurred", len(errs))
}
return results, nil
}Pattern 2: Worker Pool
Worker pool adalah pattern yang paling sering saya gunakan. Pattern ini membatasi jumlah goroutines yang aktif sehingga tidak menghabiskan semua resources:
type WorkerPool struct {
tasks chan func()
numWorkers int
wg sync.WaitGroup
}
func NewWorkerPool(numWorkers int) *WorkerPool {
wp := &WorkerPool{
tasks: make(chan func(), 100),
numWorkers: numWorkers,
}
// Start workers
for i := 0; i < numWorkers; i++ {
wp.wg.Add(1)
go func() {
defer wp.wg.Done()
for task := range wp.tasks {
task()
}
}()
}
return wp
}
func (wp *WorkerPool) Submit(task func()) {
wp.tasks <- task
}
func (wp *WorkerPool) Stop() {
close(wp.tasks)
wp.wg.Wait()
}Pattern 3: Context dengan Timeout
Di production, kita harus selalu handle timeout untuk setiap operation. Context adalah cara Go untuk mengelola cancellation dan timeout:
func FetchWithTimeout(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
return nil, fmt.Errorf("request timeout after 5 seconds")
}
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}Common Mistakes
Kesalahan paling umum yang saya lihat: goroutine leak (lupa close channel atau cancel context), race condition (akses shared state tanpa proper synchronization), dan over-engineering (terlalu banyak goroutine untuk task yang simpel). Selalu gunakan go vet dan race detector (go test -race) untuk mendeteksi masalah ini sebelum ke production.