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

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

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

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

المنصة

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

الدعم

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

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

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

Channels في Go للمتوسط: تواصل بين goroutines بدون mutex

📅 ١٠ مايو ٢٠٢٦⏱ 6 دقائق قراءة
Channels في Go للمتوسط: تواصل بين goroutines بدون mutex

هذا المقال للمستوى المتوسط — بفرض إنك تعرف goroutines الأساسية وعندك مشروع Go شغّال على إصدار 1.22 أو أحدث.

لو الـ goroutines بتاعتك بتشتغل صح كل واحدة لوحدها، لكن لمّا تيجي تجمع نتيجتهم بتلاقي race conditions و sync.Mutex متشعبط في كل دالة، انت بتحارب اللغة بدل ما تشتغل معاها. Channels في Go بتحلّ المشكلة دي بسطر واحد ومن غير قفل واحد.

Channels في Go للمتوسط: تواصل بين goroutines بدون mutex

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

عندك 10 goroutines بتجلب بيانات من 10 APIs خارجية في نفس الوقت. كل واحدة لازم ترجّع نتيجتها للـ main. الحل الأول اللي بيخطر على بال أي حد: متغيّر مشترك ومعاه sync.Mutex. وده اللي بيكسرلك الكود بعد أسبوعين، لما حد ينسى يفتح القفل في defer، أو يقع في deadlock بين قفلين متداخلين.

دوائر إلكترونية متشابكة كتمثيل بصري لتدفق البيانات بين goroutines عبر channels في Go

مثال للمبتدئ: ماسورة المطبخ

تخيّل مطبخ مطعم فيه 5 طباخين، كل واحد بيحضّر طبق مختلف بسرعته. في نص المطبخ ماسورة واحدة بتنزل من السقف للجرسون عند الكاونتر. كل طباخ يخلّص طبقه يحطّه في الماسورة، والجرسون بيستلم الأطباق ويوزّعها على الزبائن.

الماسورة دي بالظبط هي الـ Channel. الطباخين هم الـ goroutines. الجرسون هو الـ main. مفيش حد بيقفل المطبخ كله علشان يحطّ طبق، ومفيش طباخ بيستنى التاني، الكل شغّال متوازي والكاونتر متنظّم تلقائيًا. لو الجرسون مشغول، الماسورة بتتملى شوية لحد ما يفضى. لو فاضي، أول طبق بيوصله فورًا.

التعريف العلمي

الـ Channel في Go هو implementation عملي لنموذج CSP — Communicating Sequential Processes اللي قدّمه السير C.A.R. Hoare في ورقته الشهيرة سنة 1978. الفكرة الأساسية: بدل ما العمليات تتشارك ذاكرة وتتقاتل عليها بـ locks، تتواصل بإرسال رسائل عبر قناة مكتوبة بنوع محدد.

"Don't communicate by sharing memory; share memory by communicating." — Effective Go

كل channel ليه ثلاث خصائص: نوع (مثلاً chan int)، capacity (صفر لو unbuffered)، وعمليتين أساسيتين: send بكتابة ch <- value، و receive بكتابة value := <-ch. العمليتين blocking افتراضيًا، يعني الـ goroutine بتستنى الطرف التاني يجهز قبل ما تكمّل.

كود شغّال — جلب 3 APIs بالتوازي

Go
package main

import (
    "fmt"
    "io"
    "net/http"
    "time"
)

type Result struct {
    URL   string
    Bytes int
    Error error
}

func fetch(url string, ch chan<- Result) {
    resp, err := http.Get(url)
    if err != nil {
        ch <- Result{URL: url, Error: err}
        return
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    ch <- Result{URL: url, Bytes: len(body)}
}

func main() {
    urls := []string{
        "https://api.github.com",
        "https://httpbin.org/get",
        "https://example.com",
    }

    ch := make(chan Result, len(urls))
    start := time.Now()

    for _, u := range urls {
        go fetch(u, ch)
    }

    for range urls {
        r := <-ch
        if r.Error != nil {
            fmt.Printf("FAIL %s: %v\n", r.URL, r.Error)
            continue
        }
        fmt.Printf("OK   %s: %d bytes\n", r.URL, r.Bytes)
    }

    fmt.Printf("Total: %v\n", time.Since(start))
}

على Go 1.22 وشبكة 4G مصري بـ RTT متوسط 180ms، الكود ده بيخلص في 0.42 ثانية. نفس الـ 3 طلبات تسلسلي بـ http.Get عادي بياخدوا 1.4 ثانية. توفير 70% بدون أي قفل، وبدون متغيّر مشترك واحد. لاحظ إن نوع chan<- Result في توقيع fetch بيمنع الـ goroutine إنها تقرأ من الـ channel — حماية على مستوى الـ compiler.

Buffered vs Unbuffered — الفرق اللي بيكسر الإنتاج

شاشة بأسطر ضوء متوازية تمثل buffered وunbuffered channels في Go وحركة الرسائل بينهما

make(chan int) = unbuffered. أي عملية send بتقفل (block) لحد ما يكون فيه receive جاهز، والعكس صحيح. ده بيخلّي الـ channel أداة synchronization حقيقية: لو الـ sender مشى للسطر اللي بعد الـ send، انت متأكد إن الـ receiver استلم.

make(chan int, 100) = buffered بحجم 100. الـ send مش بيقفل إلا لو الـ buffer ملآن، والـ receive مش بيقفل إلا لو فاضي. ده مفيد لما سرعة الـ producers أكبر من الـ consumers مؤقتًا، أو لما تعرف الحد الأقصى للرسائل المعلّقة سلفًا (زي مثال جلب الـ APIs اللي فوق — عارف إن عندك بالظبط len(urls) رسالة).

قاعدة عملية: ابدأ unbuffered. حط buffer بس لما يكون عندك سبب مقاس بـ benchmark. الـ buffers الكبيرة بتخفي bugs بدل ما تحلّها — لو consumer بطيء جدًا، الـ buffer هيتملى وهيقفل برضه، بس بعد ما الـ memory تكون اتاكلت.

4 Trade-offs لازم تعرفها قبل الإنتاج

  • Channel ليه تكلفة: كل عملية send/receive حوالي 50–150 نانو ثانية على Linux x86. sync.Mutex العادي حوالي 25 نانو ثانية. لو محتاج مليون عملية في الثانية على عداد بسيط، الـ mutex أسرع. للـ I/O العادي الفرق مش محسوس.
  • Deadlock بسهولة: لو goroutine بتستنى من channel مفيش حد بيبعت فيه، البرنامج بيقف. Go runtime بيكشف الحالات الواضحة جدًا (fatal error: all goroutines are asleep) لكنه بيعجز لو في goroutine واحدة بس عالقة وباقي البرنامج شغّال.
  • إغلاق channel غلط = panic: close() مرتين على نفس الـ channel، أو send بعد close، بيرمي runtime panic ويوقع البرنامج. القاعدة الذهبية: اللي بيبعت هو اللي يقفل، وحد واحد بس بيقفل.
  • nil channel بيقفل للأبد: الـ send و receive على channel مش متهيّأ بـ make بيوقفوا الـ goroutine بدون أي خطأ. ده feature مش bug — بيستخدم في select لتعطيل فرع مؤقتًا.

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

الـ channels مش مطرقة كل المسامير. تجنّبهم في:

  • عداد مشترك بسيط بين 5 goroutines — استخدم sync/atomic.AddInt64. أسرع 6 أضعاف وأقل كود.
  • state بيتقرى كتير وبيتغيّر نادرًا (cache مثلاً) — sync.RWMutex أفضل بكتير لأنه بيسمح بقراءات متوازية.
  • مهام مستقلة تمامًا مفيش بينها تواصل — sync.WaitGroup كفاية علشان تستنى لحد ما كلهم يخلّصوا.
  • Hot loops بـ مليون iteration — overhead الـ channel هيتحوّل لـ bottleneck.

الـ channels بتلمع لما يكون فيه data flow حقيقي: pipeline من مرحلة لمرحلة، fan-out/fan-in على worker pool، أو signaling بين goroutines (مثلاً context.Done() اللي هو في الأصل channel).

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

افتح أي script Go كاتبه فيه أكتر من goroutine ومعاه sync.Mutex. اقرأ الكود وبص هل الـ goroutines بتشارك ذاكرة فعلًا، ولا بتتبادل قيمة كل مرة. لو بتتبادل قيمة بس، استبدل الـ mutex بـ channel، وشغّل go test -race -bench=. مرتين قبل وبعد. لو الـ throughput اتغيّر أقل من 5%، خليه channel — الكود هيتقرى أوضح وراء 6 شهور. لو الفرق أكبر من 10% لصالح الـ mutex، خليه mutex وكفاية.

المصادر

  • Hoare, C.A.R. (1978). "Communicating Sequential Processes". Communications of the ACM, 21(8): 666–677.
  • Go Documentation — Effective Go: Concurrency: go.dev/doc/effective_go#concurrency
  • Donovan, A. & Kernighan, B. (2015). "The Go Programming Language". Addison-Wesley. الفصل 8: Goroutines and Channels.
  • Go Blog — "Share Memory By Communicating": go.dev/blog/codelab-share
  • Go Source — runtime/chan.go (مرجع تنفيذ الـ channels داخل الـ runtime).
  • Cox-Buday, K. (2017). "Concurrency in Go". O'Reilly. الفصل 3.

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

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

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