النمط الأساسي
تعدّ sync.WaitGroup الـ goroutines العاملة. ترفع Add العدد، وتخفضه Done، وتتوقّف Wait حتى يصير صفرًا.
تعمل التنزيلات الثلاثة بالتزامن، فيستغرق البرنامج نحو 10 ms بدل 30. تُطبع النتائج بترتيب المدخلات لأن كل goroutine تكتب فقط في فهرسها من sizes، ولا تقرؤها main إلا بعد Wait.
القيمة الصفرية لـ WaitGroup جاهزة للاستخدام. لا حاجة لدالة بناء.
القواعد الثلاث
استدعِ Add قبل go، لا داخل الـ goroutine. إذا استدعت الـ goroutine الدالة Add بنفسها، قد تصل main إلى Wait قبل أن تبدأ أي goroutine، فترى عددًا صفريًا وتعود بينما لم يبدأ العمل. عندما تعرف العدد مسبقًا، فإن wg.Add(len(files)) مرة واحدة قبل الحلقة مكافئة.
استدعِ Done مع defer في أول سطر من الـ goroutine. الـ goroutine التي تعود مبكرًا بسبب خطأ، أو تسبّب panic، تُنقص العدّاد مع ذلك. غياب Done يترك Wait متوقّفة إلى الأبد. وإذا كانت آخر goroutine متبقّية، يبلغ وقت التشغيل عن fatal error: all goroutines are asleep مع sync.WaitGroup.Wait في الأثر.
لا تنسخ WaitGroup بعد أول استخدام أبدًا. مرّر *sync.WaitGroup إلى الدوال، أو التقط المتغيّر في إغلاق كما سبق.
تمرير WaitGroup إلى دالة
عندما يكون جسم الـ goroutine دالة مسمّاة، مرّر مؤشرًا:
مع wg sync.WaitGroup كمعامل قيمة، كان كل عامل سيستدعي Done على نسخته الخاصة وكانت main ستتوقّف في Wait إلى الأبد. يكتشف go vet ذلك قبل أن تشغّل أي شيء:
./main.go:8:24: worker passes lock by value: sync.WaitGroup contains sync.noCopy
تصميم أنظف يُبقي التزامن خارج worker كليًا: دعها دالة عادية وأجرِ حسابات Add/Done في إغلاق المستدعي. عندها يسهل اختبار worker واستدعاؤها بشكل متزامن.
العدّاد السالب
Done هي Add(-1). إذا نزل العدد تحت الصفر، يسبّب البرنامج panic:
المخرجات recovered: sync: negative WaitGroup counter. السبب المعتاد goroutine فيها defer wg.Done() تستدعي أيضًا wg.Done() صراحة في أحد المسارات.
جمع الأخطاء
لا تفعل WaitGroup إلا العدّ. للأخطاء أعطِ كل goroutine خانتها الخاصة وافحصها بعد Wait:
تتخطّى errors.Join (Go 1.20) قيم nil وتعيد nil إذا كانت كلها nil، فتجمع "خطأ لكل goroutine" دون أي حسابات إضافية.
إذا أردت إيقاف العمل المتبقّي حالما تفشل goroutine واحدة، فاستخدم golang.org/x/sync/errgroup بدلًا من ذلك. إنها WaitGroup مع أول خطأ مع سياق يُلغى عند الفشل، وg.SetLimit(n) تحدّ التزامن. تعيش خارج المكتبة القياسية، فلا يمكن تشغيلها في محرّر هذه الصفحة:
g, ctx := errgroup.WithContext(ctx)
for _, h := range hosts {
g.Go(func() error { return checkCtx(ctx, h) })
}
if err := g.Wait(); err != nil {
return err // the first error; ctx was cancelled for the others
}
مجموعة عمّال
عدد ثابت من الـ goroutines يقرأ المهام من قناة يُبقي التزامن محدودًا مهما كان عدد المهام. تخبرك الـ WaitGroup متى انتهى كل العمّال، وهو الوقت الذي يمكن فيه إغلاق قناة النتائج.
ترتيب القطع الثلاث مهم:
- يجب أن تستقبل
mainالنتائج أثناء عمل العمّال. لو استدعتmainالدالةwg.Wait()مباشرة قبل القراءة، لتوقّف العمّال عند الإرسال إلىresults، ولما وصلوا أبدًا إلىDone، ولحدث جمود في كل شيء. لهذا تعملWaitفي goroutine خاصة بها. - لا يحدث
close(results)إلا بعدWait، فلا يستطيع أي عامل الإرسال على قناة مغلقة. - يعمل مغذّي المهام أيضًا في goroutine، فيتداخل التغذية والجمع.
أي عامل عالج أي مهمة يتغيّر من تشغيل إلى آخر، لذا يرتّب البرنامج حسب المهمة قبل الطباعة. وكل ما يطبعه حتمي.
WaitGroup أم قناة أم errgroup
| الحاجة | استخدم |
|---|---|
| انتظار N goroutine، والنتائج في خانات مفهرسة | sync.WaitGroup |
| انتظار goroutine واحدة | قناة done أو قناة النتيجة نفسها |
| نتائج تتدفّق فور انتهائها | قناة تُغلق بعد wg.Wait() |
| إيقاف كل شيء عند أول خطأ | errgroup.WithContext |
| إيقاف كل شيء عند انقضاء مهلة أو إلغاء من المستدعي | context.Context مع WaitGroup أو errgroup |
تضيف Go 1.25 الدالة wg.Go(func() { ... })، التي تجري Add(1) وDone المؤجّلة نيابة عنك. أما الشيفرة لـ Go 1.24 وما قبله، ومنها محرّر هذه الصفحة، فتستخدم الشكل الصريح المعروض سابقًا.
أخطاء شائعة
wg.Add(1)داخل الـ goroutine. قد تعودWaitقبل تنفيذها.- نسيان
Doneعند عودة مبكرة. استخدم دائمًاdefer wg.Done(). - تمرير الـ WaitGroup بالقيمة. استخدم مؤشرًا؛ ويبلغ
go vetعن النسخ. - الانتظار في الـ goroutine نفسها التي يجب أن تفرّغ قناة. انقل
wg.Wait()معcloseإلى goroutine منفصلة. - إعادة استخدام WaitGroup قبل عودة
Waitالسابقة. ابدأ دورة جديدة من استدعاءاتAddفقط بعد انتهاءWait.
الأسئلة الشائعة
كيف تعمل sync.WaitGroup في Go؟
الـ WaitGroup عدّاد. تزيده wg.Add(n)، وتنقصه wg.Done() بواحد، وتتوقّف wg.Wait() حتى يصل إلى الصفر. استدعِ Add قبل تشغيل كل goroutine، وdefer wg.Done() داخلها، وWait حيث تحتاج أن ينتهي كل شيء.
هل أمرّر WaitGroup بالقيمة أم بمؤشر؟
بمؤشر (*sync.WaitGroup)، أو دع الـ goroutines تلتقطها في إغلاق. للنسخة عدّادها الخاص، فلا يصل Done على النسخة إلى الأصل أبدًا وتتوقّف Wait إلى الأبد. يبلغ go vet عن الخطأ برسالة "passes lock by value".
ما سبب رسالة "sync: negative WaitGroup counter"؟
استدعاءات Done أكثر من استدعاءات Add. عادة تستدعي goroutine الدالة Done مرتين (مرة مع defer ومرة صراحة)، أو يُتخطّى Add(1) في أحد المسارات. يسبّب البرنامج panic، لأن العدّاد لم يعد يخبرك بأي شيء صحيح.
كيف أحصل على الأخطاء من goroutines بدأتها مع WaitGroup؟
لا تحمل الـ WaitGroup نتائج ولا أخطاء. أعطِ كل goroutine خانتها الخاصة في شريحة أخطاء واجمعها بعد Wait (مثلًا بـ errors.Join)، أو استخدم golang.org/x/sync/errgroup، التي تعيد Wait فيها أول خطأ ويمكنها إلغاء الأخريات عبر سياق.