هذا المقال للمستوى المتوسط — بفرض إنك تعرف goroutines الأساسية وعندك مشروع Go شغّال على إصدار 1.22 أو أحدث.
لو الـ goroutines بتاعتك بتشتغل صح كل واحدة لوحدها، لكن لمّا تيجي تجمع نتيجتهم بتلاقي race conditions و sync.Mutex متشعبط في كل دالة، انت بتحارب اللغة بدل ما تشتغل معاها. Channels في Go بتحلّ المشكلة دي بسطر واحد ومن غير قفل واحد.
Channels في Go للمتوسط: تواصل بين goroutines بدون mutex
المشكلة باختصار
عندك 10 goroutines بتجلب بيانات من 10 APIs خارجية في نفس الوقت. كل واحدة لازم ترجّع نتيجتها للـ main. الحل الأول اللي بيخطر على بال أي حد: متغيّر مشترك ومعاه sync.Mutex. وده اللي بيكسرلك الكود بعد أسبوعين، لما حد ينسى يفتح القفل في defer، أو يقع في deadlock بين قفلين متداخلين.
مثال للمبتدئ: ماسورة المطبخ
تخيّل مطبخ مطعم فيه 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 بالتوازي
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))
}