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.
Intisari
errgroup=WaitGroup+ error pertama + pembatalan otomatis semua saudaranya.g.SetLimit(n)membatasi paralelisme dalam satu baris โ pengganti worker pool manual.singleflightmenggabungkan 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
}
| Berurutan | Dengan errgroup | |
|---|---|---|
| Latensi (80 + 120 + 40 ms) | 240 ms | 120 ms โ yang terlama |
| Satu gagal | Sisanya tidak jalan | Sisanya dibatalkan lewat ctx |
| Error yang dikembalikan | Yang pertama gagal | Yang 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 dibatasi | Angka yang wajar |
|---|---|
| Kueri database | Lebih kecil dari MaxOpenConns (Fase 4) |
| Panggilan ke API luar | Sesuai rate limit mereka, dibagi jumlah container |
| Pekerjaan CPU murni | runtime.GOMAXPROCS(0) |
| Berkas / soket | Jauh 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
| Pola | Kenapa dihindari |
|---|---|
| Satu goroutine per item, tanpa batas | Memindahkan kemacetan ke database atau ke API orang lain |
| Channel sebagai antrean pekerjaan lintas-restart | Isinya hilang saat proses mati. Butuh SQS (Fase 10) |
time.Sleep untuk menunggu goroutine | Bukan sinkronisasi, cuma harapan |
| Pustaka yang menyalakan goroutine sendiri tanpa cara menghentikannya | Tidak bisa di-shutdown rapi; muncul sebagai kebocoran |
| Concurrency untuk hal yang sudah cukup cepat | Menambah 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.