🐹 Go

Goroutinen und Kanäle in Go

Горутины и каналы в Go: конкурентность для начинающих

Горутины — это то, за что любят Go. Представь официанта в ресторане: он не ждёт пока один столик доест суп, чтобы принять заказ у другого — он обслуживает несколько столиков одновременно. Горутины в Go работают так же: лёгкие потоки выполнения, которые позволяют программе делать много дел «одновременно». И это встроено прямо в язык!

Горутины: лёгкие потоки

package main

import (
    "fmt"
    "time"
)

func sayHello(name string) {
    for i := 0; i < 3; i++ {
        fmt.Printf("Привет от %s! (%d)\n", name, i)
        time.Sleep(100 * time.Millisecond)
    }
}

func main() {
    // Обычный вызов — ждём завершения
    sayHello("Алиса")
    
    // Горутина — запускаем в фоне
    go sayHello("Борис")  // go запускает функцию как горутину
    go sayHello("Вера")   // Ещё одна горутина
    
    fmt.Println("main продолжает работу!")
    
    // Проблема: main может завершиться до горутин!
    time.Sleep(500 * time.Millisecond) // Ждём (не лучший способ)
    fmt.Println("Конец")
}

WaitGroup: правильное ожидание горутин

package main

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

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done() // Сообщаем что горутина завершена
    
    fmt.Printf("Воркер %d начал работу\n", id)
    time.Sleep(time.Duration(id) * 100 * time.Millisecond)
    fmt.Printf("Воркер %d закончил\n", id)
}

func main() {
    var wg sync.WaitGroup // Счётчик активных горутин
    
    for i := 1; i <= 5; i++ {
        wg.Add(1)           // Увеличиваем счётчик
        go worker(i, &wg)   // Запускаем горутину
    }
    
    wg.Wait() // Ждём пока счётчик не станет 0
    fmt.Println("Все воркеры завершили работу!")
}

Каналы: трубы для передачи данных

Канал (channel) — это труба, по которой горутины передают данные друг другу. Представь пневматическую почту в банке: отправляешь капсулу — получатель принимает её на другом конце.

package main

import "fmt"

func main() {
    // Создание канала
    ch := make(chan int) // Канал для передачи int
    
    // Отправляем значение в горутине
    go func() {
        ch <- 42 // Отправка в канал (стрелка влево)
    }()
    
    // Получаем значение
    value := <-ch // Получение из канала (стрелка влево = из канала)
    fmt.Println(value) // 42
    
    // Двусторонний обмен
    done := make(chan bool)
    
    go func() {
        fmt.Println("Горутина работает...")
        done <- true // Сигнал о завершении
    }()
    
    <-done // Ждём сигнала
    fmt.Println("Горутина завершена")
}

Буферизованные каналы

package main

import "fmt"

func main() {
    // Небуферизованный — блокирует пока не прочитают
    ch1 := make(chan int)
    
    // Буферизованный — не блокирует пока буфер не заполнен
    ch2 := make(chan int, 3) // буфер на 3 элемента
    
    // Можно отправить без горутины (не блокирует сразу)
    ch2 <- 1
    ch2 <- 2
    ch2 <- 3
    // ch2 <- 4 // ЗАБЛОКИРУЕТ — буфер полон!
    
    fmt.Println(<-ch2) // 1
    fmt.Println(<-ch2) // 2
    fmt.Println(<-ch2) // 3
    
    // Закрытие канала
    jobs := make(chan int, 5)
    
    go func() {
        for j := 1; j <= 5; j++ {
            jobs <- j
        }
        close(jobs) // Закрываем канал — больше отправок не будет
    }()
    
    // range читает пока канал не закрыт
    for job := range jobs {
        fmt.Printf("Получен job: %d\n", job)
    }
}

Паттерн producer-consumer

package main

import (
    "fmt"
    "sync"
)

func producer(ch chan<- int, n int) { // chan<- только для отправки
    for i := 0; i < n; i++ {
        ch <- i * i // Отправляем квадраты чисел
    }
    close(ch)
}

func consumer(id int, ch <-chan int, results chan<- string, wg *sync.WaitGroup) {
    defer wg.Done()
    for value := range ch { // Читаем пока канал не закрыт
        results <- fmt.Sprintf("Consumer %d: получил %d", id, value)
    }
}

func main() {
    jobs := make(chan int, 10)
    results := make(chan string, 20)
    var wg sync.WaitGroup
    
    // Запускаем 3 потребителя
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go consumer(i, jobs, results, &wg)
    }
    
    // Производитель
    go producer(jobs, 9)
    
    // Закрываем results когда все потребители завершились
    go func() {
        wg.Wait()
        close(results)
    }()
    
    // Выводим результаты
    for r := range results {
        fmt.Println(r)
    }
}

select: множественные каналы

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "один"
    }()
    
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "два"
    }()
    
    // select ждёт первый доступный канал
    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println("Получено из ch1:", msg1)
        case msg2 := <-ch2:
            fmt.Println("Получено из ch2:", msg2)
        }
    }
    
    // select с default — неблокирующий
    ch3 := make(chan int, 1)
    select {
    case v := <-ch3:
        fmt.Println("Получено:", v)
    default:
        fmt.Println("Канал пуст — не блокируем")
    }
    
    // select с таймаутом
    ch4 := make(chan string)
    select {
    case result := <-ch4:
        fmt.Println(result)
    case <-time.After(500 * time.Millisecond):
        fmt.Println("Таймаут! Слишком долго ждём")
    }
}

Mutex: защита от race condition

package main

import (
    "fmt"
    "sync"
)

// Race condition — несколько горутин меняют одну переменную
type SafeCounter struct {
    mu    sync.Mutex // Мьютекс — замок
    count int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()   // Захватываем замок
    c.count++
    c.mu.Unlock() // Отпускаем замок
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

func main() {
    counter := &SafeCounter{}
    var wg sync.WaitGroup
    
    // 1000 горутин одновременно увеличивают счётчик
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            counter.Increment()
        }()
    }
    
    wg.Wait()
    fmt.Println("Итоговый счётчик:", counter.Value()) // 1000
    // Без мьютекса результат был бы < 1000 (race condition!)
}

Типичные ошибки

Дедлок — все горутины заблокированы

// ОШИБКА: deadlock!
ch := make(chan int)
ch <- 1  // Блокируется — никто не читает!
v := <-ch

// ПРАВИЛЬНО: отправка в горутине
ch := make(chan int)
go func() { ch <- 1 }()
v := <-ch
fmt.Println(v)

Итого

  • ✅ Горутина — лёгкий поток, запускается через go func()
  • ✅ sync.WaitGroup — ждём завершения группы горутин
  • ✅ Канал (chan) — труба для передачи данных между горутинами
  • ✅ Буферизованный канал — make(chan T, n) — не блокирует до заполнения буфера
  • ✅ select — ожидание первого доступного из нескольких каналов
  • ✅ sync.Mutex — защита от race condition при работе с общими данными

Атомарные операции

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    // Атомарный счётчик — быстрее мьютекса для простых операций
    var counter int64
    var wg sync.WaitGroup
    
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&counter, 1)  // Потокобезопасное сложение
        }()
    }
    
    wg.Wait()
    fmt.Println("Счётчик:", atomic.LoadInt64(&counter))  // 1000
}

Паттерн Pipeline

package main

import "fmt"

// Pipeline — цепочка горутин обрабатывающих данные
func generate(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums { out <- n }
        close(out)
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in { out <- n * n }
        close(out)
    }()
    return out
}

func filterEven(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            if n%2 == 0 { out <- n }
        }
        close(out)
    }()
    return out
}

func main() {
    // Строим pipeline: числа → квадраты → только чётные
    nums := generate(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
    squares := square(nums)
    evenSquares := filterEven(squares)
    
    for n := range evenSquares {
        fmt.Print(n, " ")  // 4 16 36 64 100
    }
    fmt.Println()
}

context: отмена и таймауты

package main

import (
    "context"
    "fmt"
    "time"
)

func fetchData(ctx context.Context, id int) (string, error) {
    select {
    case <-ctx.Done():
        return "", ctx.Err()  // Контекст отменён или таймаут
    case <-time.After(time.Duration(id*100) * time.Millisecond):
        return fmt.Sprintf("Данные для %d", id), nil
    }
}

func main() {
    // Таймаут 250ms
    ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond)
    defer cancel()
    
    for i := 1; i <= 5; i++ {
        result, err := fetchData(ctx, i)
        if err != nil {
            fmt.Printf("ID %d: ошибка — %v\n", i, err)
        } else {
            fmt.Println(result)
        }
    }
}

sync.Once и sync.Pool

package main

import (
    "fmt"
    "sync"
)

// sync.Once — выполнить функцию ровно один раз
type Singleton struct {
    Data string
}

var instance *Singleton
var once sync.Once

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{Data: "Инициализирован!"}
        fmt.Println("Создаём синглтон...")
    })
    return instance
}

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            s := GetInstance()
            fmt.Println(s.Data)
        }()
    }
    wg.Wait()
    // "Создаём синглтон..." выведется только один раз!
}

Итого

  • ✅ Горутина — лёгкий поток: go func()
  • ✅ sync.WaitGroup — ждать завершения группы горутин
  • ✅ Канал (chan T) — труба для передачи данных
  • ✅ Направленные каналы: chan<- (только отправка), <-chan (только получение)
  • ✅ Буферизованный канал: make(chan T, n)
  • ✅ select — ждать первый доступный канал
  • ✅ sync.Mutex — защита от race condition
  • ✅ atomic — быстрые атомарные операции
  • ✅ context — отмена и таймауты для горутин
  • ✅ Паттерн Pipeline — цепочка горутин для обработки данных

HTTP сервер на Go

package main

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

// Счётчик запросов — thread-safe
type Counter struct {
    mu    sync.Mutex
    count int
}

func (c *Counter) Increment() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.count++
    return c.count
}

func main() {
    counter := &Counter{}
    
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        n := counter.Increment()
        fmt.Fprintf(w, "Запрос #%d\nВремя: %s\n", n, time.Now().Format("15:04:05"))
    })
    
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, `{"status":"ok","time":"%s"}`, time.Now().Format(time.RFC3339))
    })
    
    fmt.Println("Сервер запущен на :8080")
    fmt.Println("Открой http://localhost:8080 в браузере")
    http.ListenAndServe(":8080", nil)
}

Паттерн Worker Pool

package main

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

type Job struct {
    ID   int
    Data string
}

type Result struct {
    JobID  int
    Output string
    Error  error
}

func processJob(job Job) Result {
    time.Sleep(time.Millisecond * 100) // Симуляция работы
    return Result{
        JobID:  job.ID,
        Output: fmt.Sprintf("Обработан: %s", job.Data),
    }
}

func workerPool(numWorkers int, jobs <-chan Job, results chan<- Result) {
    var wg sync.WaitGroup
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func(workerID int) {
            defer wg.Done()
            for job := range jobs {
                result := processJob(job)
                fmt.Printf("  Worker %d обработал job %d\n", workerID, job.ID)
                results <- result
            }
        }(i + 1)
    }
    go func() {
        wg.Wait()
        close(results)
    }()
}

func main() {
    jobsCh := make(chan Job, 10)
    resultsCh := make(chan Result, 10)
    
    go workerPool(3, jobsCh, resultsCh)
    
    // Отправляем задачи
    for i := 1; i <= 9; i++ {
        jobsCh <- Job{ID: i, Data: fmt.Sprintf("данные_%d", i)}
    }
    close(jobsCh)
    
    // Собираем результаты
    for r := range resultsCh {
        fmt.Printf("Результат job %d: %s\n", r.JobID, r.Output)
    }
}

Практика: ограничение конкурентности

package main

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

// Semaphore — ограничение числа одновременных горутин
type Semaphore chan struct{}

func NewSemaphore(max int) Semaphore {
    return make(Semaphore, max)
}

func (s Semaphore) Acquire() { s <- struct{}{} }
func (s Semaphore) Release() { <-s }

func main() {
    sem := NewSemaphore(3) // Не более 3 одновременных задач
    var wg sync.WaitGroup
    
    for i := 1; i <= 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            sem.Acquire()
            defer sem.Release()
            
            fmt.Printf("Задача %d начата\n", id)
            time.Sleep(200 * time.Millisecond)
            fmt.Printf("Задача %d завершена\n", id)
        }(i)
    }
    
    wg.Wait()
    fmt.Println("Все задачи выполнены!")
}

Дополнительные паттерны конкурентности

package main

import (
    "fmt"
    "sync"
)

// sync.Map — потокобезопасный map
func main() {
    var m sync.Map
    var wg sync.WaitGroup
    
    // Несколько горутин пишут одновременно
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            m.Store(fmt.Sprintf("key%d", id), id*10)
        }(i)
    }
    
    wg.Wait()
    
    // Перебор элементов
    m.Range(func(key, value interface{}) bool {
        fmt.Printf("%s = %d\n", key, value)
        return true // true = продолжить перебор
    })
}