أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول

Goroutines و Channels في Go: التزامن بدون لخبطة الـ Locks

📅 ٢٦ أبريل ٢٠٢٦⏱ 5 دقائق قراءة
Goroutines و Channels في Go: التزامن بدون لخبطة الـ Locks

المستوى: متوسط — يفترض إنك كاتبت 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 قالت: خلينا ننقل البيانات بدل ما نشاركها.

شاشة محرر كود تعرض دالة Go تستخدم go keyword و channel للتزامن بين goroutines

مثال للمبتدئ: 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.

أبسط مثال شغّال

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)
    }
}

الكود ده بيشغّل 3 عمّال بالتوازي. بدل ما الـ main يستنى 300ms (100 لكل واحد متسلسل)، بيستنى 100ms تقريبًا فقط لأنهم بيشتغلوا في نفس الوقت. chan<- string معناها channel للكتابة فقط، حماية من الـ bugs.

السيناريو الواقعي: HTTP Fan-out

افتراض: عندك endpoint بيلخّص بيانات المستخدم من 4 خدمات داخلية (profile, orders, recommendations, notifications). كل خدمة بتاخد حوالي 80ms.

متسلسل: 4 × 80 = 320ms. بـ goroutines: ~85ms (أبطأ خدمة + overhead بسيط). الفرق ده بيتحول مباشرةً لـ P95 latency أقل وحمل أخف على السيرفر.

Go
func fetchAll(userID string) (Summary, error) {
    type result struct {
        kind string
        data any
        err  error
    }

    ch := make(chan result, 4)

    go func() { d, err := fetchProfile(userID); ch <- result{"profile", d, err} }()
    go func() { d, err := fetchOrders(userID);  ch <- result{"orders",  d, err} }()
    go func() { d, err := fetchRecs(userID);    ch <- result{"recs",    d, err} }()
    go func() { d, err := fetchNotifs(userID);  ch <- result{"notifs",  d, err} }()

    var s Summary
    for i := 0; i < 4; i++ {
        r := <-ch
        if r.err != nil {
            return s, r.err
        }
        s.merge(r.kind, r.data)
    }
    return s, nil
}
أنابيب صناعية متصلة كتمثيل بصري لـ pipeline من channels تنقل البيانات بين goroutines

Buffered vs Unbuffered: الفرق المهم

make(chan T) بدون رقم = unbuffered. الكتابة بتقفل لحد ما حد يقرا. مفيد لو عايز تزامن صارم. make(chan T, N) = buffered. بتسمح بـ N قيمة من غير قارئ، مفيد لما المنتج أسرع شوية من المستهلك. الـ trade-off: buffered ممكن يخفي bottlenecks لأنه يبتلع الضغط لحد ما يفيض.

الـ Trade-offs الحقيقية

  • المكسب: كود تزامن بيقرا زي كود متسلسل تقريبًا، بدون Mutex في 80% من الحالات حسب مدوّنة Go الرسمية.
  • التكلفة 1: goroutine leak. لو اتسبت بتنتظر channel مش هيكتب فيه حد، بتعيش لحد ما العملية تموت. لازم تستخدم context.WithCancel أو select مع timeout.
  • التكلفة 2: debugging أصعب. الـ stack trace ممكن يبقى كبير جدًا لما يكون عندك آلاف الـ goroutines.
  • الافتراض: الكلام ده مبني على Go 1.22+ والـ scheduler الحديث. الإصدارات الأقدم من 1.14 ما كانش فيها preemption كاملة.

متى لا تستخدم Goroutines

لو الشغل CPU-bound خالص (تشفير، تحويل صور)، ومفيش I/O، goroutines مش هتسرّعك أكتر من عدد الـ CPU cores. ساعتها runtime.GOMAXPROCS هو السقف، وفيه أحيانًا workers pool ثابت أوضح. كمان لو عندك مهمة واحدة فقط، ما فيش معنى تعمل goroutine — متسلسل أبسط وأرخص.

لو محتاج state مشترك بيتعدّل بكثرة من قراء وكتّاب كتير (counters، caches)، أحيانًا sync.Mutex أو sync/atomic أسرع من channel. ويكي Go الرسمي نفسه بيقول: "استخدم channel للتواصل بين goroutines، واستخدم mutex لحماية state بسيط".

الخطوة التالية

افتح أي endpoint في الكود بتاعك بيستدعي أكتر من service خارجية بشكل متسلسل. حوّله لـ goroutines + channel + context.WithTimeout(ctx, 2*time.Second). قِس الـ P95 قبل وبعد بـ hey أو k6. لو الفرق أقل من 30%، ده معناه إن الـ bottleneck مش في الـ I/O، ابعت النتيجة وأنا أفسّرها معاك.

المصادر

  • Effective Go — Goroutines (go.dev)
  • The Go Memory Model (go.dev)
  • Share Memory By Communicating — Go Blog
  • Mutex or Channel — Go Wiki
  • pthread_create(3) — Linux man-pages (مرجع حجم الـ stack الافتراضي للـ threads)

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة