Channel

14. Channel

تشبيه

تخيل شريط ناقل: شخص يضع الطرود في أحد الأطراف، وشخص آخر يسحبها من الطرف الآخر.

هذا هو جوهر القناة — أنبوب للتواصل الآمن بين goroutines.


المفاهيم الأساسية

المفهوم الوصف
channel أنبوب تواصل مُعلَّن، يُعلَّن بكلمة المفتاح chan
الإرسال ch <- value يكتب بيانات في القناة
الاستقبال value := <-ch يقرأ بيانات من القناة
قناة غير مخزنة تواصل متزامن؛ المرسل والمستقبِل يجب أن يكونا جاهزين في نفس الوقت
قناة مخزنة تواصل غير متزامن؛ الإرسال لا يحظر حتى امتلاء المخزن
close تغلق القناة؛ لا يُسمح بالإرسال بعد ذلك
range يستمر في الاستقبال من القناة حتى تُغلق
قيود الاتجاه إرسال فقط chan<- أو استقبال فقط <-chan

فلسفة Go: لا تتواصل بمشاركة الذاكرة؛ شارك الذاكرة بالتواصل.


البنية الأساسية والاستخدام

إنشاء قناة

GO
// قناة غير مخزنة (متزامنة)
ch := make(chan int)

// قناة مخزنة (غير متزامنة، حجم المخزن 5)
ch := make(chan string, 5)

الإرسال والاستقبال

GO
ch := make(chan int)

// الإإرسال (في goroutine آخر)
go func() {
    ch <- 42 // إرسال 42 إلى القناة
}()

// الاستقبال
value := <-ch // استقبال قيمة من القناة، يحظر حتى توفر البيانات
fmt.Println(value) // 42

إغلاق القناة

GO
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch) // إغلاق القناة، لا يُسمح بالإرسال بعد ذلك

الاستقبال مع range

GO
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
close(ch)

// range يستمر في الاستقبال حتى تُغلق القناة
for v := range ch {
    fmt.Println(v) // 10 20 30
}

قيود الاتجاه

GO
// قناة إرسال فقط
func producer(ch chan<- int) {
    ch <- 100
}

// قناة استقبال فقط
func consumer(ch <-chan int) {
    v := <-ch
    fmt.Println(v)
}

💡 نصائح


الأمثلة

مثال: تواصل Goroutine أساسي (الصعوبة ⭐)

GO
package main

import "fmt"

func main() {
    // إنشاء قناة غير مخزنة
    ch := make(chan string)

    // إطلاق goroutine لإرسال رسالة
    go func() {
        ch <- "مرحبًا، أيها الخيط الرئيسي!"
    }()

    // الـ goroutine الرئيسي يستقبل الرسالة (يحظر أثناء الانتظار)
    msg := <-ch
    fmt.Println(msg) // مرحبًا، أيها الخيط الرئيسي!
}
▶ جرّب الكود

الشرح: القناة غير المخزنة تزامن goroutines — المرسل يحظر حتى يكون المستقبِل جاهزًا.


مثال: نمط المنتج-المستهلك (الصعوبة ⭐⭐)

GO
package main

import "fmt"

// المنتج: يولّد بيانات ويرسلها إلى القناة (إرسال فقط)
func producer(id int, ch chan<- int, count int) {
    for i := 0; i < count; i++ {
        value := id*100 + i
        fmt.Printf("المنتج %d: إرسال %d\n", id, value)
        ch <- value
    }
}

// المستهلك: يستقبل بيانات من القناة ويعالجها (استقبال فقط)
func consumer(id int, ch <-chan int) {
    for v := range ch {
        fmt.Printf("المستهلك %d: معالجة %d\n", id, v)
    }
    fmt.Printf("المستهلك %d: القناة أُغلقت، خروج\n", id)
}

func main() {
    // إنشاء قناة مخزنة
    ch := make(chan int, 5)

    // بدء منتجين
    go producer(1, ch, 3)
    go producer(2, ch, 3)

    // بدء مستهلكين
    go consumer(1, ch)
    go consumer(2, ch)

    // انتظار جميع المنتجين للانتهاء (في المشاريع الحقيقية، استخدم sync.WaitGroup)
    // هنا نستخدم ببساطة sleep للعرض التوضيحي
    import_time := time.After(2 * time.Second)
    <-import_time

    close(ch) // إغلاق القناة

    // إعطاء المستهلكين وقتًا لمعالجة البيانات المتبقية
    time.Sleep(500 * time.Millisecond)
    fmt.Println("اكتملت جميع الأعمال")
}
▶ جرّب الكود

الشرح:


مثال: سمافور مع قناة و Fan-out/Fan-in (الصعوبة ⭐⭐⭐)

GO
package main

import (
    "fmt"
    "sync"
    "time"
)

// worker تعالج المهام وترسل النتائج إلى قناة النتائج
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        fmt.Printf("العامل %d: بدأ معالجة المهمة %d\n", id, job)
        time.Sleep(time.Duration(100+id*50) * time.Millisecond) // محاكاة العمل
        result := job * job
        fmt.Printf("العامل %d: المهمة %d اكتملت، النتيجة %d\n", id, job, result)
        results <- result
    }
}

func main() {
    numJobs := 10
    numWorkers := 3

    jobs := make(chan int, numJobs)     // قناة المهام
    results := make(chan int, numJobs)  // قناة النتائج

    // بدء تجمع العمال
    var wg sync.WaitGroup
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // إرسال المهام
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs) // إغلاق قناة المهام؛ حلقات range للعمال ستنتهي

    // إغلاق قناة النتائج بعد انتهاء جميع العمال
    go func() {
        wg.Wait()
        close(results)
    }()

    // جمع جميع النتائج
    total := 0
    for r := range results {
        total += r
    }

    fmt.Printf("مجموع جميع النتائج: %d\n", total)
}
▶ جرّب الكود

الشرح:


حالات الاستخدام العملية

الحالة 1: التحكم في المهلة

GO
package main

import (
    "fmt"
    "time"
)

func slowOperation(ch chan<- string) {
    time.Sleep(3 * time.Second) // محاكاة عملية بطيئة
    ch <- "اكتملت العملية"
}

func main() {
    ch := make(chan string, 1)
    go slowOperation(ch)

    // استخدام select للتحكم في المهلة
    select {
    case result := <-ch:
        fmt.Println("تم استقبال النتيجة:", result)
    case <-time.After(2 * time.Second):
        fmt.Println("انتهت مهلة العملية!") // يُفعّل بعد ثانيتين
    }
}

الشرح: time.After يُعيد قناة تُرسل قيمة بعد المدة المحددة. مع select، يُتيح تحكمًا أنيقًا في المهلة.


الحالة 2: تحديد المعدل للزاحفين المتزامنين

GO
package main

import (
    "fmt"
    "time"
)

// fetchURL محاكاة جلب URL
func fetchURL(url string) string {
    time.Sleep(500 * time.Millisecond) // محاكاة زمن الشبكة
    return "محتوى الصفحة: " + url
}

func main() {
    urls := []string{
        "https://example.com/page1",
        "https://example.com/page2",
        "https://example.com/page3",
        "https://example.com/page4",
        "https://example.com/page5",
    }

    // استخدام قناة مخزنة كسمافور، تحديد التزامن إلى 2
    semaphore := make(chan struct{}, 2)
    results := make(chan string, len(urls))

    for _, url := range urls {
        go func(u string) {
            semaphore <- struct{}{} // الحصول على السمافور (يحظر عند الامتلاء)
            fmt.Printf("بدء الجلب: %s\n", u)
            result := fetchURL(u)
            results <- result
            <-semaphore // تحرير السمافور
        }(url)
    }

    // جمع النتائج
    for i := 0; i < len(urls); i++ {
        fmt.Println(<-results)
    }

    fmt.Println("تم جلب جميع الصفحات")
}

الشرح: القناة المخزنة تعمل كسمافور، تحدد عدد goroutines المتزامنة الجارية وتمنع الكثير من الطلبات المتزامنة من إغراق الخادم المستهدف.


❓ أسئلة شائعة

س1: ماذا يحدث عند إرسال بيانات إلى قناة مغلقة؟

يسبب panic. أي عملية إرسال على قناة مغلقة تُطلق panic: send on closed channel. الاستقبال لن يسبب panic لكنه يُعيد القيمة الصفرية.

GO
ch := make(chan int, 1)
ch <- 1
close(ch)
// ch <- 2 // ❌ panic: send on closed channel
v := <-ch   // ✅ يُعيد 1 (القيمة المتبقية في المخزن)
v2 := <-ch  // ✅ يُعيد 0 (قيمة صفرية، القناة فارغة ومغلقة)

أفضل الممارسات: المرسل يجب أن يغلق القناة، وليس المستقبِل. إذا كان عدة مرسلين يتشاركون قناة، استخدم sync.Once أو قناة done إضافية للتنسيق.


س2: ماذا عن قناة nil؟

الإرسال والاستقبال يحظران بشكل دائم؛ إغلاق قناة nil يسبب panic.

GO
var ch chan int // قناة nil

// ch <- 1    // ❌ يحظر بشكل دائم
// v := <-ch  // ❌ يحظر بشكل دائم
// close(ch)  // ❌ panic: close of nil channel

القناة nil مفيدة في select — يتم تخطي الحالة المقابلة تلقائيًا:

GO
var ch chan int // قناة nil
select {
case v := <-ch:       // تُتجاهل (قناة nil)
    fmt.Println(v)
case <-time.After(1 * time.Second):
    fmt.Println("انتهت المهلة")
}

س3: ما الفرق بين القنوات غير المخزنة والمخزنة؟

الميزة غير مخزنة make(chan T) مخزنة make(chan T, n)
الإرسال يحظر عندما المستقبِل غير جاهز المخزن ممتلئ
الاستقبال يحظر عندما المرسل غير جاهز المخزن فارغ
التزامن متزامن (لقاء) غير متزامن (حتى امتلاء المخزن)
الاستخدام النموذجي إشعار إشارة، مزامنة منتج-مستهلك، طابور

س4: كيف تتحقق مما إذا كانت القناة مغلقة؟

استخدم القيمة الثانية المُعادة من عملية الاستقبال:

GO
ch := make(chan int, 1)
ch <- 42
close(ch)

v, ok := <-ch
fmt.Println(v, ok) // 42 true

v2, ok := <-ch
fmt.Println(v2, ok) // 0 false (مغلقة ولا توجد بيانات)

ok تكون false عندما تكون القناة مغلقة ولا توجد بيانات متبقية في المخزن.


📖 ملخص

النقطة الأساسية التفصيل
إنشاء قناة make(chan T) غير مخزنة، make(chan T, n) مخزنة
الإرسال ch <- value، شرط الحظر يعتمد على نوع المخزن
الاستقبال value := <-ch أو v, ok := <-ch
الإغلاق close(ch)، مسؤولية المرسل، لا يمكن الإرسال بعد الإغلاق
range for v := range ch يستمر في الاستقبال حتى الإغلاق
قيود الاتجاه chan<- إرسال فقط، <-chan استقبال فقط
select تعدد، يتعامل مع عمليات قنوات متعددة
أفضل الممارسات فضّل sync.WaitGroup لانتظار اكتمال goroutines؛ تجنب الإغلاق المزدوج

القنوات هي جوهر تزامن Go. فهم غير المخزنة مقابل المخزنة، الإغلاق/التكرار، قيود الاتجاه، وselect (الدرس التالي)، وستكون قد أتقنت جوهر التواصل المتزامن في Go.


📝 تمارين

التمرين 1: أساسيات القنوات

اكتب برنامجًا يبدأ 3 goroutines، كل واحدة تحسب مربع عدد وترسل النتيجة إلى قناة. الـ goroutine الرئيسي يستقبل ويطبع جميع النتائج.

المتطلبات:


التمرين 2: تنفيذ Fan-in بالقنوات

اكتب دالة merge(channels ...<-chan int) <-chan int تدمج عدة قنوات استقبال فقط في قناة استقبال واحدة.

المتطلبات:


التمرين 3: تجمع عمال مع مهلة

نفّذ تجمع عمال بالميزات التالية:

تلميح: ادمج القنوات و select و time.After.


الدرس التالي

15. Select →

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%