أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

Go Channels بالعربي: تواصل بين Goroutines من غير Mutex ولا Race Conditions

📅 ١٩ أبريل ٢٠٢٦⏱ 5 دقائق قراءة
Go Channels بالعربي: تواصل بين Goroutines من غير Mutex ولا Race Conditions

لو عندك 50 goroutine بتكتب على نفس الـ map، الـ sync.Mutex هيشتغل لكن هيخلّي الكود هش. Channels في Go بتحل المشكلة بمنطق مختلف: بدل ما تقفل البيانات، تنقل ملكيتها. الفرق ده بيمنع category كاملة من الـ bugs، بس له ثمن واضح في الأداء هنقيسه بالظبط.

Go Channels: الأصل في التواصل بين Goroutines

المشكلة باختصار

Goroutine في Go رخيصة جدًا — ممكن تشغّل 100 ألف منها في دقيقة. لكن لحظة ما اتنين منهم يتلامسوا على نفس المتغيّر، عندك race condition. الحل التقليدي في C++/Java هو الـ mutex، والحل الأصلي في Go هو الـ channel. شعار فريق Go نفسه: "Don't communicate by sharing memory; share memory by communicating".

رسم تخطيطي يوضّح اتصال عدة goroutines عبر channel مركزي في لغة Go

Unbuffered vs Buffered: الفرق الجوهري في سطرين

في Go نوعين من الـ channels، والفرق بينهم مش في الأداء بس — الفرق في متى بيحصل الـ synchronization.

  • Unbuffered (make(chan int)): المُرسِل بيتعلّق لحد ما المُستقبِل يستلم. ده بيضمن "handshake" — المُرسِل متأكد إن الرسالة وصلت.
  • Buffered (make(chan int, 16)): المُرسِل بيكتب ويمشي لحد ما الـ buffer يمتلى. بيفكّ الترابط الزمني بين المُرسِل والمُستقبِل.

القاعدة اللي بستخدمها: unbuffered للـ coordination، buffered للـ throughput. لو محتاج تتأكد إن goroutine خلّصت شغلها قبل ما تكمل، unbuffered. لو عندك producer أسرع من consumer وعايز تمتص الفروقات، buffered بحجم = عدد الـ cores × 2 تقريبًا.

مثال تنفيذي: Worker Pool بـ 25 سطر

السيناريو: عندك 1000 URL محتاج تعمل لها HTTP request، متقدرش تفتح 1000 request مع بعض لأن الـ target بيعمل rate-limit عند 50 request/ثانية. Worker pool بـ 10 workers يحل المشكلة.

Go
package main

import (
    "fmt"
    "net/http"
    "sync"
)

func worker(id int, jobs <-chan string, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for url := range jobs {
        resp, err := http.Get(url)
        if err != nil {
            results <- 0
            continue
        }
        results <- resp.StatusCode
        resp.Body.Close()
    }
}

func main() {
    urls := []string{"https://example.com", "https://go.dev" /* ...998 more */}
    jobs := make(chan string, len(urls))
    results := make(chan int, len(urls))
    var wg sync.WaitGroup

    for w := 1; w <= 10; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    for _, u := range urls { jobs <- u }
    close(jobs)

    wg.Wait()
    close(results)

    for r := range results { fmt.Println(r) }
}

لاحظ النقاط دي: chan<- int و <-chan string بيحدّدوا اتجاه القناة داخل الدالة — ده بيمنع الـ worker من قفل القناة بالغلط. close(jobs) بيقول للـ range في الـ worker "خلاص، ماتستنّاش تاني". sync.WaitGroup بيضمن إن الـ main بتستنّى كل الـ workers يخلّصوا قبل ما تقفل results.

شاشة محرر برمجي تعرض كود Go يمثّل worker pool يستخدم channels للتواصل

select: الأداة اللي بتمنع الـ deadlock

أكبر gotcha في channels: goroutine بتستنى رسالة على قناة مش هتوصل أبدًا. select بيحل المشكلة دي — بيخلّيك تستنّى من أكتر من قناة، مع default أو time.After كـ escape hatch.

Go
select {
case msg := <-ch:
    fmt.Println("received:", msg)
case <-time.After(2 * time.Second):
    fmt.Println("timeout — بنكمّل من غير الرسالة")
case <-ctx.Done():
    return ctx.Err()
}

القاعدة: أي channel receive في production لازم يكون عنده خطة للـ timeout أو الـ cancellation. context.Context هو الـ idiom القياسي في Go 1.7+.

Channels ضد sync.Mutex: الأرقام الحقيقية

هنا بيبدأ الـ trade-off يتّضح. في benchmark بسيط لتحديث counter مشترك من goroutines متوازية:

  • sync.Mutex: ≈ 311 نانو ثانية/عملية.
  • chan int مع goroutine واحدة بتـ serialize التحديثات: ≈ 644 نانو ثانية/عملية.
  • sync/atomic: ≈ 10–20 نانو ثانية/عملية (أسرع بكتير لعداد بسيط).

الأرقام دي مصدرها قياسات منشورة على Go primitives في سيناريو hot path (راجع المصادر في آخر المقال). الخلاصة: channel بيعمل الشغل في ضعف الزمن اللي الـ mutex بياخده تقريبًا. الثمن ده بتدفعه مقابل وضوح تصميمي أكبر، مش مقابل أي سحر.

trade-off صريح: امتى الـ channel يستاهل الثمن

بيستاهل لما:

  1. بتنقل ملكية قيمة بين goroutines — المثال الكلاسيكي: pipeline من ثلاث مراحل.
  2. عندك fan-out/fan-in patterns (workers متوازية بتشتغل على نفس الـ queue).
  3. محتاج cancellation propagation عبر شجرة من الـ goroutines — هنا context + channels هو الـ idiom الوحيد المقبول.
  4. الوضوح الهيكلي أهم من آخر 300 نانو ثانية.

ميستاهلش لما:

  1. بتحمي متغيّر بسيط من وصول متزامن (counter، boolean flag) — atomic أو Mutex أسرع وأوضح.
  2. عندك hot path فعلي بملايين العمليات في الثانية — كل نانو ثانية بتفرق.
  3. بتعمل cache بـ key-value يقرا أكتر ما يكتب — sync.RWMutex أو sync.Map أنسب.

أخطاء شائعة اتقابل معاها في مراجعات كود

  • إرسال على قناة مقفولة بيعمل panic فوري. القاعدة: اللي بيرسل هو اللي يقفل. اللي بيستقبل متيقفلش أبدًا.
  • قراءة من قناة nil بتعلّق الـ goroutine للأبد. ده مفيد في select عشان "تعطّل" فرع، بس خطر لو بالغلط.
  • Buffered channel بحجم عشوائي كبير (زي 1000) بيخبّي مشاكل الـ backpressure. حجم الـ buffer لازم يكون قرار، مش رقم عشوائي.
  • Goroutine بتكتب على قناة من غير ما حد يقرا = memory leak صامت. لازم كل sender يضمن إن في receiver هيصحّى عليه.

متى لا تستخدم channels

لو كل اللي محتاجه قفل على structure مشتركة (مش نقل ملكية)، استخدم sync.Mutex مباشرة. توثيق Go نفسه بيقول ده: "Use whichever is most expressive and/or most simple." الـ Mutex مش هزيمة — هو الأداة الصح لحماية state. الـ channel هو الأداة الصح لنقل بيانات أو تنسيق lifecycle.

الخطوة التالية

افتح أي مشروع Go عندك وابحث عن sync.Mutex. لكل واحد منهم، اسأل سؤال واحد: "أنا بحمي state، ولا بنسّق عمل بين goroutines؟" لو الإجابة الثانية، فكّر في إعادة الكتابة بـ channel. لو الأولى، خلّيه mutex وما تتكلّفش.

المصادر

  • Go Wiki: Use a sync.Mutex or a channel? — التوصية الرسمية من فريق Go.
  • A Tour of Go: Channels — التوثيق الأساسي لأنواع القنوات.
  • Go Blog: Pipelines and cancellation — نمط fan-out/fan-in الأصلي.
  • Channels vs Mutexes In Go — the Big Showdown — مقارنة أداء مفصّلة.
  • Atomic vs Mutex vs Cond vs Channels — Benchmarking the Hot Path — مصدر أرقام الـ 311/644 نانو ثانية.
  • When to Use sync vs. channel in Go — مرجع قرار الاختيار بين الاتنين.

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة