المستوى: متوسط — يفترض إنك كاتبت Go قبل كده على مستوى دوال وحلقات، ولسه مدخلتش في التزامن بشكل جدي.
لو بتفكر تعمل crawler يجيب 200 رابط، أو API gateway يكلم 4 خدمات في نفس الوقت، الطريقة التقليدية بـ threads وlocks بتكلفك ذاكرة كبيرة وbugs صعبة الـ debug. Go اختارت طريق مختلف: Goroutines بحجم كيلوبايتات بدل ميجابايتات، وChannels تنقل البيانات بدل ما تشارك ذاكرة وتقفلها بـ Mutex.
Goroutines و Channels: ليه Go اختارت طريق مختلف عن باقي اللغات؟
المشكلة باختصار
الـ thread العادي في Linux بيحجز افتراضيًا ذاكرة stack حوالي 8MB حسب توثيق pthread_create. لو شغّلت 10,000 thread، ده 80GB من العنوان الافتراضي، وكل context switch بيكلف microseconds. لما بتشارك متغيرات بين threads، لازم Mutex، ولو نسيت Unlock أو ترتبت العمليات غلط بتاخد deadlock في الإنتاج بدل الـ dev. Go قالت: خلينا ننقل البيانات بدل ما نشاركها.
مثال للمبتدئ: Goroutine زي عامل في مطعم
تخيّل مطعم فيه 4 طلبات مختلفة. بدل ما الشيف يعمل واحد ورا التاني، بينادي على 4 عمال — كل واحد ياخد طلب. الشيف هنا اسمه scheduler، والعمال هم goroutines. الـ channel ده شباك التسليم: لما العامل يخلّص، يحط الطبق على الشباك، والشيف ياخده بترتيب وصول.
الفرق إن في Go، تشغيل عامل جديد بيكلفك حوالي 2KB ذاكرة بس في البداية حسب توثيق Go الرسمي، وممكن توصل لمليون goroutine على جهاز عادي. ده مش ممكن مع threads عادية.
التعريف العلمي بدقة
Goroutine هي دالة بتشتغل بالتوازي مع باقي الكود تحت إدارة Go runtime scheduler. الـ scheduler بيوزّع goroutines على عدد محدود من OS threads (عادة بعدد الـ CPU cores). ده اسمه M:N scheduling: M goroutines على N threads.
Channel هو هيكل بيانات typed يربط goroutines: واحد بيكتب فيه (ch <- value) والتاني بيقرأ منه (v := <-ch). الـ channel بيضمن إن العملية thread-safe بدون Mutex، لأن الترتيب نفسه happens-before في الـ memory model بتاع Go.
أبسط مثال شغّال
package main
import (
"fmt"
"time"
)
func worker(id int, ch chan<- string) {
time.Sleep(100 * time.Millisecond)
ch <- fmt.Sprintf("worker %d done", id)
}
func main() {
ch := make(chan string, 3)
for i := 1; i <= 3; i++ {
go worker(i, ch)
}
for i := 0; i < 3; i++ {
fmt.Println(<-ch)
}
}