errgroup.Group.SetLimit dapat memblokir goroutine yang memanggil Group.Go. Limit diterapkan sebelum worker goroutine baru dimulai, sehingga group yang penuh menerapkan backpressure pada saat task submission alih-alih membangun internal queue.
Perilaku ini penting ketika submission merupakan bagian dari control path lain. Loop yang tampak meluncurkan pekerjaan secara asynchronous dapat berhenti di g.Go(...) sampai salah satu active function selesai.
Limit berada pada admission
Zero-value errgroup.Group tidak memiliki concurrency limit. Setelah SetLimit(n), paling banyak n function yang dimulai oleh Go aktif secara bersamaan. Negative limit mengembalikan unlimited admission, sedangkan zero limit mencegah setiap pemanggilan Go berikutnya memulai function.
Boundary utamanya ada pada pemanggilan Go:
var g errgroup.Group
g.SetLimit(2)
for _, job := range jobs {
job := job
g.Go(func() error {
return process(job)
})
}
err := g.Wait()Dengan dua pemanggilan process yang berjalan lama, invocation g.Go ketiga tidak memasukkan job ke queue di dalam errgroup. Submitting goroutine menunggu sampai capacity tersedia. Setelah itu barulah Go mendaftarkan dan memulai function berikutnya.
Ini membuat SetLimit berbeda dari worker pool yang ditopang buffered channel. Buffered queue dapat menerima pending job melebihi jumlah active worker. errgroup tidak memiliki pending-task buffer seperti itu dalam mekanisme ini; pressure diteruskan langsung ke caller.
Active function menentukan capacity
Jumlah yang dikonfigurasi berlaku untuk active function di dalam group, bukan untuk operating-system thread dan bukan untuk seluruh goroutine dalam process. Sebuah function menempati satu group slot sejak admission sampai function tersebut return.
Task yang memulai goroutine tambahan secara internal tetap menempati satu slot errgroup. Child goroutine tersebut berada di luar admission accounting milik group kecuali juga dimulai melalui group yang sama.
Demikian pula, function yang terblokir pada network I/O, channel receive, mutex, atau timer tetap dianggap active dari sudut pandang group. Slot baru dilepas ketika function yang diberikan ke Go return.
Perbedaan ini dapat menghasilkan CPU utilization rendah walaupun group sudah mencapai cap yang dikonfigurasi. Limit membatasi in-flight group function, bukan runnable CPU work.
Submission order dapat menjadi bagian dari latency
Karena Go dapat blocking, code yang melakukan enumerasi task terikat pada task completion. Pertimbangkan producer yang membaca record dan langsung mensubmit satu function untuk setiap record:
for scanner.Scan() {
record := append([]byte(nil), scanner.Bytes()...)
g.Go(func() error {
return store(record)
})
}Setelah limit penuh, scanning berhenti di g.Go. Source tidak lagi dikonsumsi sampai active call ke store return.
Coupling ini dapat berguna. Ia membatasi jumlah work yang di-admit dari source dan dapat mencegah kumpulan waiting goroutine tumbuh tanpa batas. Namun coupling ini juga dapat memengaruhi upstream behavior: socket read dapat berhenti, file scanning dapat berhenti, dan channel producer pada akhirnya dapat blocking karena consumer tidak lagi menerima data.
Backpressure yang dihasilkan bersifat struktural, bukan kebetulan. Menaikkan limit mengubah baik maximum active work maupun titik saat submitter berhenti.
Cancellation tidak mengubah admission menjadi queue
errgroup.WithContext mengaitkan derived context dengan group. Non-nil error pertama yang dikembalikan group function membatalkan context tersebut, dan Wait mengembalikan non-nil error pertama setelah seluruh group function selesai.
SetLimit tidak menambahkan task cancellation semantics pada function yang menunggu untuk di-admit melalui Go. Admission operation adalah blocking call yang dikendalikan group capacity. Code yang membutuhkan non-blocking admission decision memiliki operasi terpisah: TryGo.
TryGo hanya memulai function ketika capacity sedang tersedia dan melaporkan hasilnya sebagai boolean:
if !g.TryGo(func() error {
return flush(batch)
}) {
recordSaturation()
}Ini mengubah overload behavior secara signifikan. Go menunggu capacity; TryGo menolak immediate admission. Keduanya tidak membuat buffered backlog yang dimiliki errgroup.
Limit tidak boleh diubah secara dinamis saat work aktif
SetLimit adalah configuration untuk group boundary, bukan live resizing control. Package contract mengharuskan limit tidak diubah ketika goroutine dalam group masih aktif.
Desain yang membutuhkan runtime concurrency adjustment karena itu memerlukan synchronization boundary berbeda, seperti semaphore yang dikelola terpisah atau worker set. Memperlakukan SetLimit sebagai knob yang dinaikkan dan diturunkan secara concurrent dengan active call melanggar API contract.
Pola yang stabil adalah menetapkan limit sebelum mensubmit work untuk group tersebut dan membiarkannya tidak berubah sampai active set selesai.
Nested submission dapat deadlock saat cap tercapai
Blocking admission memiliki konsekuensi tajam ketika active group function mensubmit work tambahan ke limited group yang sama dan menunggu submission path tersebut dapat maju.
Dengan limit satu, masalahnya langsung terlihat:
var g errgroup.Group
g.SetLimit(1)
g.Go(func() error {
g.Go(func() error {
return nil
})
return nil
})
_ = g.Wait()Outer function memegang satu-satunya slot. Nested g.Go menunggu free slot. Slot tidak dapat tersedia karena outer function tidak dapat return sampai blocked g.Go selesai. Tidak ada internal queue atau scheduler yang dapat memutus dependency tersebut.
Limit yang lebih tinggi dapat menunjukkan pola yang sama ketika semua slot ditempati function yang mencoba nested admission ke group yang sama.
Concurrency cap karena itu juga merupakan dependency constraint. Code yang secara recursive memperluas work membutuhkan admission structure yang tidak membuat occupied worker menunggu slot yang secara kolektif mereka sendiri pegang.
Bounded concurrency juga membatasi sebagian retained state
Direct submission melalui limited group dapat mengurangi memory growth dibandingkan memulai satu goroutine per item lalu menempatkan semaphore di dalam setiap goroutine. Pada pola terakhir, setiap submitted item sudah dapat memiliki goroutine, closure, captured data, dan stack state yang menunggu semaphore.
Dengan SetLimit, caller berhenti meng-admit function setelah cap penuh. Jika caller juga membuat per-task data hanya mendekati waktu submission, jumlah task-specific state yang resident secara bersamaan dapat tetap lebih dekat ke active-work bound.
Ini bukan universal memory bound. Producer mungkin sudah memegang seluruh input collection, active function dapat mengalokasikan data arbitrer, dan nested work dapat membuat state independen. Properti yang berguna lebih sempit: SetLimit tidak membutuhkan satu waiting goroutine untuk setiap task yang belum di-admit.
Cap mengubah control flow, bukan hanya parallelism
Concurrency limit sering dijelaskan sebagai jumlah operasi simultan. Untuk errgroup.SetLimit, efek operasionalnya lebih luas: capacity menentukan apakah submitting goroutine dapat melanjutkan melewati Go.
Boundary ini memengaruhi source consumption, latency propagation, memory yang dipertahankan pending work, dan keamanan nested submission. TryGo menyediakan rejection boundary berbeda ketika memblokir submitter tidak dapat diterima.
Limit paling akurat diperlakukan sebagai synchronous admission control yang melekat pada Group.Go, dengan setiap slot dipertahankan sampai admitted function return.