Channel
14. Channel
تشبيه
تخيل شريط ناقل: شخص يضع الطرود في أحد الأطراف، وشخص آخر يسحبها من الطرف الآخر.
- الشريط الناقل هو القناة (channel)
- وضع الطرد هو الإرسال (
ch <- value) - سحب الطرد هو الاستقبال (
<-ch) - عندما يكون الشريط ممتلئًا، يجب على المرسل الانتظار؛ عندما يكون فارغًا، يجب على المستقبِل الانتظار
هذا هو جوهر القناة — أنبوب للتواصل الآمن بين goroutines.
المفاهيم الأساسية
| المفهوم | الوصف |
|---|---|
| channel | أنبوب تواصل مُعلَّن، يُعلَّن بكلمة المفتاح chan |
| الإرسال | ch <- value يكتب بيانات في القناة |
| الاستقبال | value := <-ch يقرأ بيانات من القناة |
| قناة غير مخزنة | تواصل متزامن؛ المرسل والمستقبِل يجب أن يكونا جاهزين في نفس الوقت |
| قناة مخزنة | تواصل غير متزامن؛ الإرسال لا يحظر حتى امتلاء المخزن |
| close | تغلق القناة؛ لا يُسمح بالإرسال بعد ذلك |
| range | يستمر في الاستقبال من القناة حتى تُغلق |
| قيود الاتجاه | إرسال فقط chan<- أو استقبال فقط <-chan |
فلسفة Go: لا تتواصل بمشاركة الذاكرة؛ شارك الذاكرة بالتواصل.
البنية الأساسية والاستخدام
إنشاء قناة
// قناة غير مخزنة (متزامنة)
ch := make(chan int)
// قناة مخزنة (غير متزامنة، حجم المخزن 5)
ch := make(chan string, 5)
الإرسال والاستقبال
ch := make(chan int)
// الإإرسال (في goroutine آخر)
go func() {
ch <- 42 // إرسال 42 إلى القناة
}()
// الاستقبال
value := <-ch // استقبال قيمة من القناة، يحظر حتى توفر البيانات
fmt.Println(value) // 42
إغلاق القناة
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
close(ch) // إغلاق القناة، لا يُسمح بالإرسال بعد ذلك
الاستقبال مع range
ch := make(chan int, 3)
ch <- 10
ch <- 20
ch <- 30
close(ch)
// range يستمر في الاستقبال حتى تُغلق القناة
for v := range ch {
fmt.Println(v) // 10 20 30
}
قيود الاتجاه
// قناة إرسال فقط
func producer(ch chan<- int) {
ch <- 100
}
// قناة استقبال فقط
func consumer(ch <-chan int) {
v := <-ch
fmt.Println(v)
}
💡 نصائح
- القنوات غير المخزنة تضمن المزامنة بين المرسل والمستقبِل — البيانات تنتقل مباشرة من أحدهما إلى الآخر
- القنوات المخزنة تحظر عند الإرسال عندما يكون المخزن ممتلئًا، وتحظر عند الاستقبال عندما يكون المخزن فارغًا
- الإرسال إلى قناة مغلقة يسبب panic
- الاستقبال من قناة مغلقة يُعيد القيمة الصفرية دون حظر
- لا تغلق القناة من جانب المستقبِل؛ المرسل يجب أن يغلقها (ما لم يكن هناك مرسل واحد فقط)
- استخدم
v, ok := <-chللتحقق مما إذا كانت القناة مغلقة (okتكونfalseعند الإغلاق وعدم وجود بيانات)
الأمثلة
مثال: تواصل Goroutine أساسي (الصعوبة ⭐)
package main
import "fmt"
func main() {
// إنشاء قناة غير مخزنة
ch := make(chan string)
// إطلاق goroutine لإرسال رسالة
go func() {
ch <- "مرحبًا، أيها الخيط الرئيسي!"
}()
// الـ goroutine الرئيسي يستقبل الرسالة (يحظر أثناء الانتظار)
msg := <-ch
fmt.Println(msg) // مرحبًا، أيها الخيط الرئيسي!
}
الشرح: القناة غير المخزنة تزامن goroutines — المرسل يحظر حتى يكون المستقبِل جاهزًا.
مثال: نمط المنتج-المستهلك (الصعوبة ⭐⭐)
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("اكتملت جميع الأعمال")
}
الشرح:
- القنوات المخزنة تسمح للمنتجين بالاستمرار في الإنتاج طالما أن المخزن لم يمتلئ، دون انتظار المستهلكين
range chيستمر في الاستقبال حتى تُغلق القناة- قيود الاتجاه
chan<-و<-chanتمنع سوء الاستخدام وقت الترجمة
مثال: سمافور مع قناة و Fan-out/Fan-in (الصعوبة ⭐⭐⭐)
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)
}
الشرح:
- Fan-out: عدة عمال يقرأون من نفس قناة المهام
- Fan-in: عدة عمال يكتبون في نفس قناة النتائج
WaitGroupتضمن انتهاء جميع العمال قبل إغلاق قناة النتائج- هذا نمط تجمع العمال المتزامن الشائع في Go
حالات الاستخدام العملية
الحالة 1: التحكم في المهلة
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: تحديد المعدل للزاحفين المتزامنين
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 لكنه يُعيد القيمة الصفرية.
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.
var ch chan int // قناة nil
// ch <- 1 // ❌ يحظر بشكل دائم
// v := <-ch // ❌ يحظر بشكل دائم
// close(ch) // ❌ panic: close of nil channel
القناة nil مفيدة في select — يتم تخطي الحالة المقابلة تلقائيًا:
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: كيف تتحقق مما إذا كانت القناة مغلقة؟
استخدم القيمة الثانية المُعادة من عملية الاستقبال:
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 الرئيسي يستقبل ويطبع جميع النتائج.
المتطلبات:
- استخدم قناة غير مخزنة
- استخدم
WaitGroupلضمان اكتمال جميع goroutines - الإخراج يجب أن يبدو مثل:
النتائج: [1, 4, 9]
التمرين 2: تنفيذ Fan-in بالقنوات
اكتب دالة merge(channels ...<-chan int) <-chan int تدمج عدة قنوات استقبال فقط في قناة استقبال واحدة.
المتطلبات:
- المدخلات: عدة
<-chan int - المخرجات:
<-chan intواحدة تحتوي على بيانات من جميع القنوات المدخلة - نفّذ باستخدام goroutines و
WaitGroup - عندما تُغلق جميع القنوات المدخلة، يجب أن تُغلق قناة المخرجات أيضًا
التمرين 3: تجمع عمال مع مهلة
نفّذ تجمع عمال بالميزات التالية:
- ابدأ N goroutines عاملة
- كل مهمة لها مهلة 500ms
- إذا انتهت مهلة المهمة، سجّلها وتجاوزها
- عدّ المهام الناجحة وتلك التي انتهت مهلتها
تلميح: ادمج القنوات و select و time.After.



