GoCasts آموزش Go به زبان ساده

بیش از ۱۰۰۰ شرکت‌کننده یادگیری Go و Backend رو از امروز شروع کن
ثبت‌نام دوره + تیم‌سازی

گوروتین چیست؟ آموزش Goroutine و همزمانی در گولنگ

یکی از قدرتمندترین ویژگی‌های Go، پشتیبانی داخلی از برنامه‌نویسی همزمان است. در این مقاله، Goroutine‌ها را به صورت کامل بررسی می‌کنیم و یاد می‌گیریم چگونه برنامه‌های همزمان بنویسیم.

Goroutine چیست؟

Goroutine یک thread سبک‌وزن است که توسط Go runtime مدیریت می‌شود. برخلاف thread‌های سیستم‌عامل که سنگین هستند، Goroutine‌ها:

  • حافظه اولیه کمی مصرف می‌کنند (~2KB در مقابل ~1MB برای thread)
  • ایجاد و تخریب سریع دارند
  • توسط Go scheduler مدیریت می‌شوند
  • می‌توانید میلیون‌ها Goroutine همزمان داشته باشید

اولین Goroutine

برای ایجاد Goroutine، کافی است کلمه go را قبل از فراخوانی تابع بگذارید:

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    fmt.Println("سلام از Goroutine!")
}

func main() {
    go sayHello()  // اجرا در Goroutine جدید

    // صبر کنیم تا Goroutine اجرا شود
    time.Sleep(100 * time.Millisecond)
    fmt.Println("سلام از main!")
}

خروجی:

سلام از Goroutine!
سلام از main!

نکته مهم: اگر time.Sleep را حذف کنید، برنامه قبل از اجرای Goroutine تمام می‌شود!

Goroutine با تابع ناشناس

package main

import (
    "fmt"
    "time"
)

func main() {
    // Goroutine با تابع ناشناس
    go func() {
        fmt.Println("تابع ناشناس در Goroutine")
    }()

    // Goroutine با پارامتر
    message := "سلام"
    go func(msg string) {
        fmt.Println(msg)
    }(message)

    time.Sleep(100 * time.Millisecond)
}

چند Goroutine همزمان

package main

import (
    "fmt"
    "time"
)

func worker(id int) {
    fmt.Printf("Worker %d شروع کرد\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d تمام شد\n", id)
}

func main() {
    // ایجاد 5 worker همزمان
    for i := 1; i <= 5; i++ {
        go worker(i)
    }

    // صبر برای اتمام همه
    time.Sleep(2 * time.Second)
    fmt.Println("همه worker ها تمام شدند")
}

خروجی (ترتیب ممکن است متفاوت باشد):

Worker 5 شروع کرد
Worker 1 شروع کرد
Worker 3 شروع کرد
Worker 2 شروع کرد
Worker 4 شروع کرد
Worker 1 تمام شد
Worker 5 تمام شد
Worker 2 تمام شد
Worker 3 تمام شد
Worker 4 تمام شد
همه worker ها تمام شدند

WaitGroup - صبر برای اتمام Goroutine‌ها

استفاده از time.Sleep روش مناسبی نیست. از sync.WaitGroup استفاده کنید:

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // اعلام اتمام کار

    fmt.Printf("Worker %d شروع کرد\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d تمام شد\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)        // افزایش شمارنده
        go worker(i, &wg)
    }

    wg.Wait()            // صبر تا شمارنده صفر شود
    fmt.Println("همه کارها تمام شد!")
}

Channel - ارتباط بین Goroutine‌ها

Channel‌ها راه اصلی ارتباط بین Goroutine‌ها هستند:

package main

import "fmt"

func main() {
    // ایجاد channel
    ch := make(chan string)

    // ارسال در Goroutine
    go func() {
        ch <- "سلام از Goroutine!"
    }()

    // دریافت در main
    message := <-ch
    fmt.Println(message)
}

Channel با بافر

package main

import "fmt"

func main() {
    // Channel با ظرفیت 3
    ch := make(chan int, 3)

    // ارسال بدون بلاک شدن (تا ظرفیت)
    ch <- 1
    ch <- 2
    ch <- 3

    fmt.Println(<-ch)  // 1
    fmt.Println(<-ch)  // 2
    fmt.Println(<-ch)  // 3
}

بستن Channel

package main

import "fmt"

func producer(ch chan int) {
    for i := 1; i <= 5; i++ {
        ch <- i
    }
    close(ch)  // اعلام پایان ارسال
}

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

    go producer(ch)

    // دریافت تا بسته شدن channel
    for num := range ch {
        fmt.Println(num)
    }
}

مثال عملی: دانلود همزمان

package main

import (
    "fmt"
    "io"
    "net/http"
    "sync"
    "time"
)

type Result struct {
    URL   string
    Size  int
    Error error
}

func fetchURL(url string, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()

    start := time.Now()

    resp, err := http.Get(url)
    if err != nil {
        results <- Result{URL: url, Error: err}
        return
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        results <- Result{URL: url, Error: err}
        return
    }

    elapsed := time.Since(start)
    fmt.Printf("%s: %d bytes in %v\n", url, len(body), elapsed)

    results <- Result{URL: url, Size: len(body)}
}

func main() {
    urls := []string{
        "https://golang.org",
        "https://google.com",
        "https://github.com",
    }

    results := make(chan Result, len(urls))
    var wg sync.WaitGroup

    start := time.Now()

    for _, url := range urls {
        wg.Add(1)
        go fetchURL(url, results, &wg)
    }

    // بستن channel بعد از اتمام همه
    go func() {
        wg.Wait()
        close(results)
    }()

    // جمع‌آوری نتایج
    totalSize := 0
    for result := range results {
        if result.Error == nil {
            totalSize += result.Size
        }
    }

    fmt.Printf("\nTotal: %d bytes in %v\n", totalSize, time.Since(start))
}

Select - انتخاب بین چند Channel

package main

import (
    "fmt"
    "time"
)

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

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "پیام از channel 1"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "پیام از channel 2"
    }()

    // دریافت از هر کدام که زودتر آماده شود
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }
}

Select با Timeout

package main

import (
    "fmt"
    "time"
)

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

    go func() {
        time.Sleep(2 * time.Second)
        ch <- "پاسخ"
    }()

    select {
    case msg := <-ch:
        fmt.Println("دریافت:", msg)
    case <-time.After(1 * time.Second):
        fmt.Println("Timeout!")
    }
}

الگوی Worker Pool

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()

    for job := range jobs {
        fmt.Printf("Worker %d پردازش job %d\n", id, job)
        time.Sleep(500 * time.Millisecond)  // شبیه‌سازی کار
        results <- job * 2
    }
}

func main() {
    numJobs := 10
    numWorkers := 3

    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)
    var wg sync.WaitGroup

    // ایجاد worker ها
    for w := 1; w <= numWorkers; w++ {
        wg.Add(1)
        go worker(w, jobs, results, &wg)
    }

    // ارسال کارها
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs)

    // صبر و بستن results
    go func() {
        wg.Wait()
        close(results)
    }()

    // جمع‌آوری نتایج
    for result := range results {
        fmt.Println("نتیجه:", result)
    }
}

Mutex - جلوگیری از Race Condition

وقتی چند Goroutine به داده مشترک دسترسی دارند:

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

func main() {
    counter := &Counter{}
    var wg sync.WaitGroup

    // 1000 Goroutine همزمان
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }

    wg.Wait()
    fmt.Println("مقدار نهایی:", counter.Value())  // همیشه 1000
}

تفاوت Goroutine با Thread

ویژگی Goroutine Thread OS
حافظه اولیه ~2KB ~1MB
ایجاد میکروثانیه میلی‌ثانیه
مدیریت Go runtime سیستم‌عامل
ارتباط Channel Shared memory
تعداد میلیون‌ها هزاران
Context switch سریع کند

نکات مهم

۱. همیشه Goroutine‌ها را مدیریت کنید

// بد - Goroutine leak
func bad() {
    go func() {
        for {
            // کار بی‌پایان
        }
    }()
}

// خوب - با امکان توقف
func good(done chan bool) {
    go func() {
        for {
            select {
            case <-done:
                return
            default:
                // کار
            }
        }
    }()
}

۲. از Race Detector استفاده کنید

go run -race main.go
go test -race ./...

۳. پارامترها را صریح پاس دهید

// بد - مشکل closure
for i := 0; i < 5; i++ {
    go func() {
        fmt.Println(i)  // همیشه 5 چاپ می‌شود!
    }()
}

// خوب - پاس دادن مقدار
for i := 0; i < 5; i++ {
    go func(n int) {
        fmt.Println(n)  // 0, 1, 2, 3, 4
    }(i)
}

جمع‌بندی

مفهوم کاربرد
go func() ایجاد Goroutine
sync.WaitGroup صبر برای اتمام
chan ارتباط بین Goroutine‌ها
select انتخاب بین channel‌ها
sync.Mutex محافظت از داده مشترک

قدم‌های بعدی

منابع

بیش از ۱۰۰۰ شرکت‌کننده یادگیری Go و Backend رو از امروز شروع کن
ثبت‌نام دوره + تیم‌سازی