لو سكربتك بيستدعي 10,000 endpoint بالتنابع وبياخد 47 دقيقة، انت بتدفع تكلفة قرار غلط في 3 سطور كود. Go بـ goroutines و channels بيخلّي نفس الشغل يخلص في 38 ثانية، بدون thread pool وبدون مكتبة خارجية. المقال ده هيوريك بالظبط ليه، وإزاي تكتب الكود ده صح من غير ما يقع في deadlock.
Goroutines و Channels في Go: التوازي بتكلفة 4KB لكل مهمة
المشكلة باختصار
تخيّل إنك بتشتغل في مكتبة عامة، وعندك 10,000 طلب استعارة كتاب في الصبح. لو موظف واحد بيخدم كل طلب لوحده، الناس هتقعد في الطابور ساعتين. الحل البديهي إنك تجيب 10,000 موظف، بس ده كارثة في التكلفة والإدارة. الحل الذكي: 50 موظف بيتعاملوا بمرونة مع الطلبات اللي بتيجي على "كاونتر مشترك" — كل موظف بياخد طلب، يخلصه، يرجع للكاونتر ياخد التالي.
ده بالظبط اللي goroutines بتعمله. كل goroutine بتاكل 4KB ذاكرة في البداية (مقابل 1-2MB لكل OS thread)، والـ runtime بتاع Go بيوزّعهم تلقائيًا على عدد cores الـ CPU بتاعك. الكاونتر المشترك في القصة دي اسمه channel.
التعريف العلمي: ليه Goroutines مش OS Threads
Goroutine هي وحدة تنفيذ خفيفة بتديرها runtime بتاعة Go نفسها، مش kernel الـ OS مباشرةً. الـ Go scheduler بيستخدم نموذج M:N — يعني M goroutines بتشتغل على N OS threads، حسب ورقة "Scheduling Multithreaded Computations by Work Stealing" لـ Blumofe و Leiserson من MIT 1999 اللي اتبنى عليها scheduler الـ Go.
الـ Channel هي قناة typed بين goroutines، مبنية على نموذج CSP اختصار Communicating Sequential Processes اللي قدّمه Tony Hoare في ورقة CACM 1978. الفكرة الأساسية في Go شعار رسمي مكتوب في الـ blog الرسمي: "Don't communicate by sharing memory; share memory by communicating." يعني بدل ما تستخدم Mutex على متغير مشترك، خلّي الـ goroutines تبعت لبعض البيانات عبر channel.
المثال التنفيذي: 10K طلب HTTP بالتوازي
الكود ده بياخد 10,000 URL وبيجيبهم بالتوازي بحد أقصى 50 worker متزامن. لاحظ الـ flow: feeder بيحط الـ jobs في channel، الـ workers بيسحبوا منه، والـ results بترجع في channel تاني.
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
func main() {
urls := make([]string, 10000)
for i := range urls {
urls[i] = fmt.Sprintf("https://httpbin.org/anything/%d", i)
}
jobs := make(chan string, 100)
results := make(chan int, 100)
var wg sync.WaitGroup
for w := 1; w <= 50; w++ {
wg.Add(1)
go worker(jobs, results, &wg)
}
go func() {
for _, url := range urls {
jobs <- url
}
close(jobs)
}()
go func() {
wg.Wait()
close(results)
}()
start := time.Now()
total := 0
for r := range results {
total += r
}
fmt.Printf("Done %d in %v\n", total, time.Since(start))
}
func worker(jobs <-chan string, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for url := range jobs {
resp, err := http.Get(url)
if err != nil {
results <- 0
continue
}
resp.Body.Close()
results <- 1
}
}