Go: قنوات الاتصال

القنوات هي العناصر الأساسية لنموذج التزامن في لغة Go — «لا تتواصل عن طريق مشاركة الذاكرة؛ بل شارك الذاكرة عن طريق التواصل». تجعل القنوات نقل البيانات بين goroutines آمنًا وأنيقًا تمامًا مثل استخدام الأنابيب.

إذا كانت «الغوروتينات» هي «الأشخاص» في عالم التزامن بلغة Go، فإن القنوات هي «خطوط الهاتف» التي تربط بينهم. في هذا الدرس، ستتقن جميع الاستخدامات الأساسية للقنوات وستتعرف على الأخطاء الشائعة.

1. ستتعلم



2. قصة حقيقية لمهندس برمجيات الخلفية

(1) المشكلة: كود الذاكرة المشتركة + الميوتكس قبيح وعرضة لحدوث حالات تعطل

أليس هي مهندسة «الخلفية» في فريق المدفوعات، وتحتاج إلى تمرير بيانات «المعاملات» بين عدة «غوروتينات»:

«استخدمت sync.Mutex لحماية شريحة []Transaction مشتركة، حيث تقوم 5 غوروتينات بالكتابة إليها و3 غوروتينات بالقراءة منها. ومن بين 100 سطر من الكود، هناك 30 سطرًا عبارة عن أوامر Lock/Unlock. وعندما رأى المراجع ذلك، قال على الفور: «لا يمكنني مراجعة هذا الكود — لن أجرؤ على لمس سطر واحد منه.»»

يبدو كودها كما يلي:

GO
// Shared memory approach: lock on every read/write, hard to maintain
type TransactionPool struct {
    mu     sync.Mutex
    items  []Transaction
}

func (p *TransactionPool) Add(t Transaction) {
    p.mu.Lock()
    defer p.mu.Unlock()
    p.items = append(p.items, t)
}

func (p *TransactionPool) Get() Transaction {
    p.mu.Lock()
    defer p.mu.Unlock()
    if len(p.items) == 0 {
        return Transaction{}
    }
    item := p.items[0]
    p.items = p.items[1:]
    return item
}

(2) الحل بلغة Go: تمرير البيانات عبر قناة

GO
// channel_approach.go
package main

import "fmt"

type Transaction struct {
    ID     string
    Amount float64
}

func main() {
    // Create an unbuffered channel
    ch := make(chan Transaction)

    // Producer goroutine
    go func() {
        tx := Transaction{ID: "TXN-001", Amount: 99.99}
        ch <- tx  // Send (blocks until receiver is ready)
        fmt.Println("Producer: send complete")
    }()

    // Consumer
    tx := <-ch  // Receive (blocks until sender is ready)
    fmt.Printf("Consumer: received %s ($%.2f)\n", tx.ID, tx.Amount)
}

الناتج:

TEXT 📖 للعرض فقط
Consumer: received TXN-001 ($99.99)
Producer: send complete

(3) الأداء: القناة مقابل الذاكرة المشتركة

البعد الذاكرة المشتركة + موتكس القناة
حجم الكود 30 سطراً من كود القوالب سطر واحد ch <- v
أمان الخيوط القفل/إلغاء القفل يدويًّا ضمانات اللغة الأصلية
الترابط عالي (مترابط عبر متغيرات مشتركة) منخفض (يعتمد فقط على واجهة chan)
قابلية الاختبار يتطلب استخدام موتكس وهمي الاختبار مباشرةً باستخدام قناة
خطر حدوث تعطل مرتفع (مشاكل في ترتيب القفل) يمكن اكتشاف بعض حالات التعطل في مرحلة التحويل البرمجي
💡 نصيحة: مقولة شهيرة: «لا تتواصل من خلال مشاركة الذاكرة؛ بل شارك الذاكرة من خلال التواصل.» — لا تشارك البيانات؛ بل انقل البيانات من غوروتين إلى آخر عبر القنوات، مع التأكد من أن كل جزء من البيانات له مالك واحد فقط.



3. إنشاء القنوات والعمليات الأساسية

(1) إنشاء قناة

GO
package main

import "fmt"

func main() {
    // Unbuffered channel (synchronous)
    ch1 := make(chan int)

    // Buffered channel (asynchronous, capacity 3)
    ch2 := make(chan string, 3)

    // nil channel (cannot be used directly)
    var ch3 chan float64

    fmt.Printf("ch1: %T, unbuffered\n", ch1)
    fmt.Printf("ch2: %T, buffer=%d\n", ch2, cap(ch2))
    fmt.Printf("ch3: %T, nil=%v\n", ch3, ch3 == nil)
}

(2) الإرسال والاستلام

GO
ch := make(chan int)

// Send: ch <- القيمة
ch <- 42  // Unbuffered: blocks until someone receives

// Receive: القيمة := <-ch
القيمة := <-ch  // Blocks until someone sends

// Discard received القيمة
<-ch  // Receive only, ignore القيمة (used for synchronization signals)

▶ مثال: سلوك التزامن لقناة غير مزودة بمخزن مؤقت

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)

    go func() {
        fmt.Println("goroutine: ready to send...")
        ch <- "hello"  // Blocks until main receives
        fmt.Println("goroutine: send complete")
    }()

    time.Sleep(1 * time.Second)  // Simulate delay
    fmt.Println("main: ready to receive...")
    msg := <-ch
    fmt.Printf("main: received %q\n", msg)
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
goroutine: ready to send...
main: ready to receive...
goroutine: send complete
main: received "hello"
🔥 خطأ شائع (القنوات المتزامنة بدون مخزن مؤقت): يجب أن يكون كل من المرسل والمستقبل جاهزين في نفس الوقت؛ وإلا فسيتم حظر أحد الطرفين. القنوات بدون مخزن مؤقت = متزامنة — حيث ينتظر المرسل المستقبل، وينتظر المستقبل المرسل؛ ولا يستمر كلاهما في الوقت نفسه إلا بعد «التصافح» بينهما.



4. القنوات غير المخزنة مقابل القنوات المخزنة

(1) السلوك غير المتزامن للقنوات المخزنة مؤقتًا

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int, 3)  // Capacity 3

    go func() {
        for i := 1; i <= 5; i++ {
            ch <- i
            fmt.Printf("Sent %d (len=%d)\n", i, len(ch))
        }
        close(ch)
    }()

    time.Sleep(500 * time.Millisecond)

    for v := range ch {
        fmt.Printf("Received %d\n", v)
        time.Sleep(200 * time.Millisecond)
    }
}

الناتج:

TEXT 📖 للعرض فقط
Sent 1 (len=0)
Sent 2 (len=1)
Sent 3 (len=2)
Sent 4 (len=3)
Sent 5 (len=4)
Received 1
Received 2
Received 3
Received 4
Received 5

▶ مثال: إيقاف التشغيل عند امتلاء المخزن المؤقت

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int, 2)  // Capacity 2

    ch <- 1  // ✅ Does not block (has space)
    ch <- 2  // ✅ Does not block (has space)
    // ch <- 3  // ❌ Blocks! Buffer is full, waiting for receiver

    go func() {
        time.Sleep(100 * time.Millisecond)
        fmt.Println("goroutine: starting to receive")
        <-ch  // Frees up one slot
    }()

    fmt.Println("main: sending 3")
    ch <- 3  // Now does not block (goroutine has received one)
    fmt.Println("main: send complete")
    close(ch)

    for v := range ch {
        fmt.Printf("v=%d\n", v)
    }
}
▶ جرّب الكود

(3) مقارنة بين الأنظمة الخالية من المخزن المؤقت والأنظمة المزودة بمخزن مؤقت

مقال خاص بدون تخزين مؤقت make(chan T) مع تخزين مؤقت make(chan T, n)
عملية الإرسال التوقف حتى يصبح جهاز الاستقبال جاهزًا عدم التوقف إذا كان هناك مساحة متوفرة في المخزن المؤقت
سلوك الاستلام الحظر حتى يصبح المرسل جاهزًا عدم الحظر عندما تكون البيانات موجودة في المخزن المؤقت
متزامن/غير متزامن متزامن (المصافحة) غير متزامن (قائمة الانتظار)
السعة 0 n
السيناريوهات النموذجية إشارات التزامن، والتنسيق بين الغوروتينات قوائم انتظار المهام، وخطوط الإنتاج


5. التكرار حسب القيمة القريبة والنطاق

(1) إغلاق: إغلاق القناة

GO
package main

import "fmt"

func main() {
    ch := make(chan int, 3)
    ch <- 1
    ch <- 2
    close(ch)  // Cannot send after closing

    // Can still receive remaining data after closing
    v1 := <-ch  // 1
    v2 := <-ch  // 2
    v3 := <-ch  // 0 (zero value, ok=false)
    fmt.Println(v1, v2, v3)
}

▶ مثال: تقوم وظيفة «comma-ok» بالتحقق مما إذا كانت القناة مغلقة أم لا

GO
package main

import "fmt"

func main() {
    ch := make(chan int, 2)
    ch <- 10
    ch <- 20
    close(ch)

    // comma-ok: ok=false means channel is closed and has no data
    v, ok := <-ch
    fmt.Printf("v=%d, ok=%v\n", v, ok)  // v=10, ok=true

    v, ok = <-ch
    fmt.Printf("v=%d, ok=%v\n", v, ok)  // v=20, ok=true

    v, ok = <-ch
    fmt.Printf("v=%d, ok=%v\n", v, ok)  // v=0, ok=false (closed and empty)
}
▶ جرّب الكود

(3) إجراء تكرار عبر القناة باستخدام حلقة for-range

GO
package main

import "fmt"

func main() {
    ch := make(chan string, 3)
    ch <- "Alice"
    ch <- "Bob"
    ch <- "Charlie"
    close(ch)

    // for range automatically exits when the channel is closed
    for name := range ch {
        fmt.Printf("Hello, %s!\n", name)
    }

    // Equivalent to:
    // for {
    //     name, ok := <-ch
    //     if !ok { break }
    //     fmt.Println(name)
    // }
}
🔥 خطأ شائع: سيؤدي إرسال البيانات إلى قناة مغلقة إلى حدوث حالة ذعر. كما أن إغلاق قناة مغلقة بالفعل سيؤدي أيضًا إلى حدوث حالة ذعر. لا داعي لأن يقلق المستقبل بشأن إغلاق القناة — فـ for range يتولى ذلك تلقائيًا. ويقع على عاتق المرسل دائمًا مسؤولية استدعاء close.



6. قيود الاتجاه

(1) تحدد معلمة الدالة اتجاه القناة

GO
package main

import "fmt"

// Write-only channel (can only send)
func producer(out chan<- int) {
    for i := 1; i <= 3; i++ {
        out <- i  // ✅ Can only send
    }
    close(out)
}

// Read-only channel (can only receive)
func consumer(in <-chan int) {
    for v := range in {
        fmt.Printf("Received: %d\n", v)  // ✅ Can only receive
    }
}

func main() {
    ch := make(chan int, 3)
    go producer(ch)   // Automatically converts to chan<- int
    consumer(ch)       // Automatically converts to <-chan int
}

▶ مثال: القيود الاتجاهية في الممارسة العملية (نمط خط الأنابيب)

GO
package main

import (
    "fmt"
    "strings"
)

// Stage 1: Write-only
func stage1(names []string, out chan<- string) {
    for _, name := range names {
        out <- strings.ToUpper(name)
    }
    close(out)
}

// Stage 2: Read-write (read from in, process, write to out)
func stage2(in <-chan string, out chan<- string) {
    for name := range in {
        out <- "Hello, " + name + "!"
    }
    close(out)
}

// Stage 3: Read-only
func stage3(in <-chan string) {
    for msg := range in {
        fmt.Println(msg)
    }
}

func main() {
    ch1 := make(chan string, 3)
    ch2 := make(chan string, 3)

    names := []string{"Alice", "Bob", "Charlie"}

    go stage1(names, ch1)
    go stage2(ch1, ch2)
    stage3(ch2)
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
Hello, ALICE!
Hello, BOB!
Hello, CHARLIE!

(3) الاستخدام مع قيود اتجاهية

الإعلان الأذونات الغرض
ch chan T القراءة/الكتابة إعلان المتغيرات
ch chan<- T للكتابة فقط معلمات دالة producer
ch <-chan T للقراءة فقط معلمات دالة المستهلك
💡 نصيحة: يتم التحقق من اتجاهية القناة فقط في وقت التحويل البرمجي — ولن يؤدي تمرير قناة ثنائية الاتجاه إلى معلمة مخصصة للكتابة فقط إلى حدوث خطأ (بسبب التحويل الضمني التلقائي)، لكن محاولة استقبال البيانات عبر قناة مخصصة للكتابة فقط ستؤدي إلى حدوث خطأ في وقت التحويل البرمجي.



7. مقدمة في تقنية «Select Multiplexing»

(1) أساسيات SELECT

GO
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "from ch1"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "from ch2"
    }()

    select {
    case msg := <-ch1:
        fmt.Println(msg)
    case msg := <-ch2:
        fmt.Println(msg)
    case <-time.After(300 * time.Millisecond):
        fmt.Println("Timeout")
    }
}
💡 نصيحة: select مشابه لـ switch، لكنه يُستخدم للقنوات — حيث يتم تنفيذ أول قناة تصبح جاهزة ضمن case. وإذا أصبحت عدة قنوات جاهزة في نفس الوقت، يتم اختيار واحدة منها عشوائيًا. يُعد select الأداة المثلى للبرمجة المتزامنة في لغة Go؛ وسنتناولها بالتفصيل في الدرس 15.

▶ مثال: تطبيق التحكم في مهلة الانتظار في جمل SELECT

GO
package main

import (
    "fmt"
    "time"
)

func longOperation(result chan<- string) {
    time.Sleep(3 * time.Second)
    result <- "Done"
}

func main() {
    result := make(chan string)

    go longOperation(result)

    الاختيار {
    case res := <-result:
        fmt.Println(res)
    case <-time.After(1 * time.Second):
        fmt.Println("Operation timed out!")
    }
}
▶ جرّب الكود

الناتج:

TEXT 📖 للعرض فقط
Operation timed out!


8. سيناريوهات التعطل الشائعة

GO
// Deadlock 1: Unbuffered channel with send but no receive
func deadlock1() {
    ch := make(chan int)
    ch <- 42  // fatal error: all goroutines are asleep - deadlock!
}

// Deadlock 2: main goroutine waiting on itself
func deadlock2() {
    ch := make(chan int)
    <-ch  // fatal error: deadlock
}

// Deadlock 3: Multiple goroutines waiting on each other
func deadlock3() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        <-ch1  // Wait for ch1
        ch2 <- 1
    }()

    <-ch2  // Wait for ch2
    ch1 <- 1  // Never reaches this line
}

(1) مرجع سريع: 4 أنواع من سيناريوهات التعطل

السيناريو السبب الحل
إرسال فقط، بدون استقبال حجب القناة غير المخزنة مؤقتًا التأكد من وجود مستهلكين
الاستقبال فقط، بدون إرسال القناة فارغة ولا يوجد مرسل تأكد من وجود منتج
الجوروتينات التي تنتظر بعضها البعض A تنتظر قناة B، وB تنتظر قناة A إعادة تصميم ترتيب التبعية
العمليات على القنوات الفارغة الحظر الدائم على قناة فارغة أو من قناة فارغة تهيئة قناة


9. مثال كامل: مسار «المنتج-المستهلك»

GO
// pipeline.go
package main

import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)

// Work unit
type Job struct {
    ID      int
    Payload string
}

type Result struct {
    Job      Job
    Output   string
    Err      error
    Duration time.Duration
}

// Producer: generates jobs (write-only channel)
func producer(jobs chan<- Job, count int) {
    for i := 1; i <= count; i++ {
        jobs <- Job{
            ID:      i,
            Payload: fmt.Sprintf("task-%d", i),
        }
        time.Sleep(time.Duration(rand.Intn(50)) * time.Millisecond)
    }
    close(jobs)
}

// Consumer: processes jobs (read jobs, write results)
func consumer(id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()
    for job := range jobs {
        start := time.Now()

        // Simulate processing
        time.Sleep(time.Duration(50+rand.Intn(100)) * time.Millisecond)
        output := fmt.Sprintf("consumer-%d processed %s", id, job.Payload)

        results <- Result{
            Job:      job,
            Output:   output,
            Duration: time.Since(start),
        }
    }
}

// Collector: collects and prints results (read-only channel)
func collector(results <-chan Result, done chan<- struct{}) {
    var total time.Duration
    count := 0

    for r := range results {
        count++
        total += r.Duration
        status := "OK"
        if r.Err != nil {
            status = "ERR"
        }
        fmt.Printf("[%s] Job#%d: %s (%v)\n",
            status, r.Job.ID, r.Output, r.Duration)
    }

    if count > 0 {
        fmt.Printf("\nTotal %d tasks, avg %.2f ms\n",
            count, float64(total.Milliseconds())/float64(count))
    }
    close(done)
}

func main() {
    const (
        jobCount     = 20
        workerCount  = 3
    )

    jobs := make(chan Job, 10)
    results := make(chan Result, 10)
    done := make(chan struct{})

    // 1 producer
    go producer(jobs, jobCount)

    // N consumers
    var wg sync.WaitGroup
    for w := 1; w <= workerCount; w++ {
        wg.Add(1)
        go consumer(w, jobs, results, &wg)
    }

    // Wait for all consumers to finish, then close results
    go func() {
        wg.Wait()
        close(results)
    }()

    // Collector (main goroutine)
    collector(results, done)
    <-done
}

النتيجة المتوقعة:

TEXT 📖 للعرض فقط
[OK] Job#1: consumer-2 processed task-1 (85ms)
[OK] Job#2: consumer-1 processed task-2 (92ms)
[OK] Job#3: consumer-3 processed task-3 (78ms)
...
[OK] Job#20: consumer-1 processed task-20 (112ms)

Total 20 tasks, avg 87.45 ms
100%
sequenceDiagram
    participant P as Producer
    participant C as Channel
    participant W as Consumer

    Note over P: Sender is responsible for close
    P->>C: ch <- job (send)
    activate C
    Note over C: Unbuffered: sync wait<br/>Buffered: enqueue
    C->>W: job := <-ch (receive)
    deactivate C
    Note over W: Process task
    W-->>P: Continue loop
    Note over P: close(ch)
    Note over C: Remaining data readable
    Note over W: for range auto-exit
🔥 خطأ شائع: يحتوي هذا المثال على 3 قنوات (jobs/results/done) و3 أنواع من القيود الاتجاهية (للكتابة فقط، للقراءة فقط، للقراءة والكتابة). تضمن القيود الاتجاهية في وقت التحويل البرمجي عدم إمكانية استلام البيانات في المُنتِج — وهذه ميزة تصميمية في نظام الأنواع بلغة Go تهدف إلى ضمان أمان التزامن.



❓ أسئلة شائعة

س ما الفرق بين القنوات ذات المخزن المؤقت والقنوات غير ذات المخزن المؤقت؟
ج في القنوات غير ذات المخزن المؤقت، يجب أن يكون الإرسال والاستقبال جاهزين في نفس الوقت (مصافحة متزامنة)؛ وإلا فإن القناة تتعطل. أما القناة ذات المخزن المؤقت فلا تتعطل عند الإرسال إذا لم يكن المخزن المؤقت ممتلئًا، ولا تتعطل عند الاستقبال إذا لم يكن المخزن المؤقت فارغًا. تُستخدم القنوات غير المزودة بمخزن مؤقت للتنسيق المتزامن، بينما تُستخدم القنوات المزودة بمخزن مؤقت لقوائم الانتظار غير المتزامنة.
س هل لا يزال بإمكانك إرسال رسائل بعد إغلاق القناة؟
ج لا. سيؤدي إرسال رسالة إلى قناة مغلقة إلى حدوث حالة ذعر. بعد الإغلاق، يمكنك الاستمرار في تلقي أي بيانات متبقية؛ وبمجرد قراءة جميع البيانات، يتم إرجاع قيمة صفر. في v, ok := <-ch، تشير قيمة ok التي تساوي false إلى أن القناة مغلقة ولم يتبق فيها أي بيانات.
س ما الغرض من القيود الاتجاهية chan<- و<-chan؟
ج إنها تقيد اتجاه القنوات في توقيعات الدوال — مما يضمن في مرحلة الترجمة ألا يتلقى المنتجون البيانات عن طريق الخطأ وألا يرسل المستهلكون البيانات عن طريق الخطأ. وسيقوم مُترجم لغة Go بالإبلاغ عن خطأ في مرحلة الترجمة بدلاً من التسبب في حدوث حالة ذعر في مرحلة التشغيل.
س ما هو التنفيذ الأساسي للقناة؟
ج من الناحية الفنية، تتكون القناة من مخزن مؤقت حلقي بالإضافة إلى طابورين للانتظار (طابور المرسل وطابور المستقبل). عند إرسال البيانات، إذا كان هناك مستلمون في انتظار في طابور المستقبل، يتم تمريرها مباشرةً؛ وإلا، يتم وضعها في المخزن المؤقت أو في طابور المرسل. انظر البنية hchan في كود مصدر لغة Go runtime/chan.go.
س كيف يستخدم select القنوات؟
ج يستمع select إلى قنوات متعددة في آن واحد؛ ويقوم بتنفيذ case لأي قناة تصبح جاهزة أولاً. وإذا أصبحت عدة قنوات جاهزة في نفس الوقت، فإنه يختار واحدة منها عشوائيًا. وإذا لم تكن أي من القنوات جاهزة وتم تحديد حالة default، يتم تنفيذ حالة default؛ وإلا، يتم تعليق العملية والانتظار. select هو المفتاح للتحكم في مهلة الانتظار والعمليات غير المعطلة.
س كيف يمكن تجنب حالات التعطل المتبادل للقنوات؟
ج هناك أربع قواعد: يجب أن يكون للقنوات غير المخزنة مؤقتًا مستقبل مطابق؛ انتبه لترتيب التبعيات بين الغوروتينات؛ استخدم select مع جملة default لتنفيذ العمليات غير المعطلة؛ استخدم close لإخطار المستقبل بأن القناة قد أُغلقت.
س هل يمكن استخدام القناة كإشارة؟
ج نعم. يمكن للقناة المزودة بمخزن مؤقت أن تعمل كإشارة: يتم تهيئة sem := make(chan struct{}, 10)، وإرسال sem <- struct{}{} قبل العملية، واستلام <-sem عند الانتهاء. وتقوم القناة بحجب المرور عندما يمتلئ المخزن المؤقت — مما يوفر تحديدًا طبيعيًا للمعدل.
س متى ينبغي استخدام القناة (channel)، ومتى ينبغي استخدام الموتكس (mutex)؟
ج نقل البيانات → القناة؛ حماية البيانات → الموتكس. وبالتحديد: نقل الملكية (نقل البيانات من غوروتين إلى آخر) → القناة؛ حماية الحالة المشتركة (وصول عدة غوروتينات إلى المتغير نفسه) → الموتكس. هذان العنصران ليسا بديلين عن بعضهما البعض؛ بل هما متكاملان.

📖 ملخص


📝 تمارين

  1. المسألة الأساسية (الصعوبة ⭐): اكتب برنامجًا يبدأ «جوروتين» لإرسال الأرقام العشرة من 0 إلى 9 إلى قناة غير مخزنة مؤقتًا، بينما تقوم «الجوروتين» الرئيسية باستلامها وطباعتها. لاحظ التناوب في ترتيب الإرسال والاستلام.

  2. مشكلة متقدمة (درجة الصعوبة ⭐⭐): قم بتنفيذ نمط التوزيع: يقوم منتج بإرسال 100 مهمة إلى قناة؛ قم بتشغيل 5 goroutines مستهلكة لقراءة المهام ومعالجتها من نفس القناة، على أن يقوم كل مستهلك بطباعة معرّفه الخاص ورقم المهمة. يجب عليك استخدام قناة مخزنة مؤقتًا، وclose، وfor range.

  3. التحدي (الصعوبة ⭐⭐⭐): قم بتنفيذ مسار من ثلاث مراحل: المرحلة 1 تولد أعدادًا صحيحة عشوائية ([]int)؛ المرحلة 2 تستبعد الأعداد الزوجية؛ المرحلة 3 تحسب مجموع المربعات. كل مرحلة عبارة عن goroutine مستقلة متصلة عبر القنوات. المتطلبات: (1) استخدم قيود الاتجاه بين المراحل؛ (2) ادعم التعديل الديناميكي لعدد المراحل؛ (3) قم بتنفيذ خروج أنيق باستخدام select.

Web-Tutorial.com

فريق Web-Tutorial التقني

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

100%