โ† Semua pembelajaran / Go Nol โ†’ Enterprise
Fase 2 ยท Concurrency

errgroup & pola concurrency praktis

Setelah goroutine, channel, dan mutex, yang tersisa adalah merangkainya jadi pola yang berulang. Empat pola berikut menutupi hampir semua kebutuhan concurrency di aplikasi web.

Sumber asli pkg.go.dev Resmi Rangkuman ~7 menit baca

Intisari

  • errgroup = WaitGroup + error pertama + pembatalan otomatis semua saudaranya.
  • g.SetLimit(n) membatasi paralelisme dalam satu baris โ€” pengganti worker pool manual.
  • singleflight menggabungkan permintaan identik yang datang bersamaan jadi satu โ€” obat cache stampede.
  • Fan-out ke beberapa layanan mengubah latensi total dari jumlah jadi yang terlama.
  • Semua pola ini butuh context. Tanpa itu, kegagalan satu cabang tidak menghentikan cabang lain.

Pola 1: fan-out โ€” beberapa panggilan sekaligus

import "golang.org/x/sync/errgroup"

func (s *Layanan) Dasbor(ctx context.Context, uid int64) (Dasbor, error) {
	var (
		profil   Profil
		pesanan  []Pesanan
		notif    int
	)

	g, ctx := errgroup.WithContext(ctx)

	g.Go(func() (err error) {
		profil, err = s.profil.Ambil(ctx, uid)
		return err
	})
	g.Go(func() (err error) {
		pesanan, err = s.pesanan.Terbaru(ctx, uid, 5)
		return err
	})
	g.Go(func() (err error) {
		notif, err = s.notif.Hitung(ctx, uid)
		return err
	})

	if err := g.Wait(); err != nil {
		return Dasbor{}, err
	}
	return Dasbor{Profil: profil, Pesanan: pesanan, Notifikasi: notif}, nil
}
BerurutanDengan errgroup
Latensi (80 + 120 + 40 ms)240 ms120 ms โ€” yang terlama
Satu gagalSisanya tidak jalanSisanya dibatalkan lewat ctx
Error yang dikembalikanYang pertama gagalYang pertama gagal

errgroup.WithContext mengembalikan context baru yang dibatalkan begitu salah satu g.Go mengembalikan error. Itulah yang membuat dua panggilan lain berhenti alih-alih tetap membebani database untuk hasil yang sudah pasti dibuang. Kalau kamu memakai errgroup.Group{} tanpa WithContext, kamu kehilangan setengah manfaatnya.

Pola 2: batas paralelisme

g, ctx := errgroup.WithContext(ctx)
g.SetLimit(10)          // maksimal 10 goroutine berjalan bersamaan

for _, url := range daftarURL {
	g.Go(func() error {
		return unduh(ctx, url)
	})
}
if err := g.Wait(); err != nil { ... }

Satu baris SetLimit menggantikan seluruh worker pool manual dari materi channel. Angkanya bukan tebakan โ€” ia diturunkan dari sumber daya yang paling terbatas:

Yang dibatasiAngka yang wajar
Kueri databaseLebih kecil dari MaxOpenConns (Fase 4)
Panggilan ke API luarSesuai rate limit mereka, dibagi jumlah container
Pekerjaan CPU murniruntime.GOMAXPROCS(0)
Berkas / soketJauh di bawah ulimit -n

Pola 3: yang tercepat menang

// Dua sumber, ambil yang duluan menjawab. Cocok untuk cache + database.
func ambilCepat(ctx context.Context, id int64) (Produk, error) {
	ctx, batal := context.WithCancel(ctx)
	defer batal()                       // menghentikan yang kalah

	type hasil struct {
		p   Produk
		err error
	}
	ch := make(chan hasil, 2)           // BERBUFFER: yang kalah tidak bocor

	go func() { p, err := dariCache(ctx, id); ch <- hasil{p, err} }()
	go func() { p, err := dariDB(ctx, id);    ch <- hasil{p, err} }()

	var terakhir error
	for range 2 {
		h := <-ch
		if h.err == nil {
			return h.p, nil
		}
		terakhir = h.err
	}
	return Produk{}, terakhir
}

Perhatikan make(chan hasil, 2). Kalau channel-nya tanpa buffer, goroutine yang kalah akan menggantung selamanya mencoba mengirim ke channel yang tidak dibaca siapa pun โ€” kebocoran goroutine persis seperti di materi pertama fase ini. Buffer sebesar jumlah pengirim adalah kebiasaan yang layak dipakai setiap kali.

Pola 4: singleflight โ€” menggabungkan permintaan kembar

import "golang.org/x/sync/singleflight"

type Layanan struct {
	grup  singleflight.Group
	cache *redis.Client
}

func (s *Layanan) Ambil(ctx context.Context, id int64) (Produk, error) {
	kunci := fmt.Sprintf("produk:%d", id)

	v, err, _ := s.grup.Do(kunci, func() (any, error) {
		// Hanya SATU goroutine masuk ke sini per kunci, berapa pun
		// jumlah pemanggil bersamaan. Sisanya menunggu hasilnya.
		return s.dariDB(ctx, id)
	})
	if err != nil {
		return Produk{}, err
	}
	return v.(Produk), nil
}

Ini obat untuk cache stampede. Saat kunci cache populer kedaluwarsa, seribu permintaan yang datang bersamaan semuanya gagal cache dan semuanya menembak database dengan kueri yang sama persis. singleflight membuat satu yang jalan dan 999 sisanya menunggu jawaban yang sama. Ini salah satu perubahan tiga baris dengan dampak terbesar pada aplikasi bertrafik baca tinggi โ€” muncul lagi di Fase 9.

Pola yang sebaiknya kamu hindari

PolaKenapa dihindari
Satu goroutine per item, tanpa batasMemindahkan kemacetan ke database atau ke API orang lain
Channel sebagai antrean pekerjaan lintas-restartIsinya hilang saat proses mati. Butuh SQS (Fase 10)
time.Sleep untuk menunggu goroutineBukan sinkronisasi, cuma harapan
Pustaka yang menyalakan goroutine sendiri tanpa cara menghentikannyaTidak bisa di-shutdown rapi; muncul sebagai kebocoran
Concurrency untuk hal yang sudah cukup cepatMenambah kelas bug tanpa menambah nilai โ€” ukur dulu

Aturan yang menutup fase ini: di aplikasi web, sebagian besar kode tidak perlu menulis concurrency sama sekali. net/http sudah paralel per permintaan, dan connection pool database sudah aman dipakai bersamaan. Goroutine eksplisit hanya dibutuhkan untuk fan-out di dalam satu permintaan dan untuk pekerjaan latar. Kalau ragu, tulis kode berurutan dulu โ€” lalu ukur.

Latihan: tulis fungsi yang memanggil tiga endpoint publik (misalnya tiga path berbeda di https://go.dev) secara berurutan dan catat durasinya. Ubah ke errgroup dan bandingkan. Lalu tambahkan g.SetLimit(1) dan buktikan durasinya kembali seperti versi berurutan โ€” itu cara termudah memahami apa yang sebenarnya dilakukan batas paralelisme.

Rangkuman ini sengaja dipangkas ke bagian yang dipakai di roadmap. Buka sumber aslinya saat kamu butuh detail lengkap atau referensi parameter.