MidТеория5 min

Каналы

Небуферизованные и буферизованные каналы, направленность, select, дедлоки

Каналы (Channels)

Что такое канал?

Канал (channel) -- это типизированный механизм коммуникации между горутинами. Каналы обеспечивают синхронизацию и передачу данных одновременно. Это реализация принципа Go: "Не общайтесь через разделяемую память -- разделяйте память через общение" (Share memory by communicating).

Горутина A ──► [  канал  ] ──► Горутина B
               ch <- val        val := <-ch

Каналы -- это first-class values в Go: их можно передавать как аргументы, возвращать из функций, хранить в структурах.

Создание каналов

package main

import "fmt"

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

    // Buffered channel — async until buffer is full
    ch2 := make(chan string, 10)

    // Channel of channels — meta pattern
    ch3 := make(chan chan int)

    // Channel of structs — for signaling (zero-size)
    done := make(chan struct{})

    fmt.Printf("ch1: %T, ch2: %T, ch3: %T, done: %T\n", ch1, ch2, ch3, done)
}

Небуферизованные каналы (Unbuffered)

Небуферизованный канал не имеет внутренней ёмкости. Отправка и получение блокируют горутину до тех пор, пока обе стороны не будут готовы. Это делает передачу синхронной -- гарантируется, что данные переданы.

Горутина A:  ch <- 42   ──► БЛОК ─────────► продолжение
                                    │
Горутина B:          ... ──► <-ch ──┘ (обе разблокированы одновременно)
package main

import "fmt"

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

    go func() {
        // This blocks until someone reads from ch
        ch <- 42
        fmt.Println("Sent 42")
    }()

    // This blocks until someone writes to ch
    value := <-ch
    fmt.Println("Received:", value)
    // Output:
    // Received: 42
    // Sent 42
}

Ключевой момент: В небуферизованном канале отправитель блокируется до момента, когда получатель готов принять значение, и наоборот. Это создаёт точку синхронизации (happens-before guarantee).

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

Буферизованный канал имеет внутреннюю очередь FIFO. Отправка блокируется только когда буфер полон, а получение -- только когда буфер пуст.

Буфер размера 3:

ch <- 1   [1|_|_]  — не блокирует
ch <- 2   [1|2|_]  — не блокирует
ch <- 3   [1|2|3]  — не блокирует
ch <- 4   [1|2|3]  — БЛОКИРУЕТ (буфер полон)

<-ch      [2|3|_]  — возвращает 1, разблокирует отправителя
package main

import "fmt"

func main() {
    // Buffered channel with capacity 3
    ch := make(chan string, 3)

    // These don't block — buffer has space
    ch <- "first"
    ch <- "second"
    ch <- "third"

    fmt.Println("Buffer length:", len(ch)) // 3
    fmt.Println("Buffer capacity:", cap(ch)) // 3

    // Read in FIFO order
    fmt.Println(<-ch) // first
    fmt.Println(<-ch) // second
    fmt.Println(<-ch) // third
}

Когда использовать буферизованные каналы

Сценарий Буфер
Синхронизация (handshake) 0 (unbuffered)
Известное число задач N (количество задач)
Пул воркеров N (количество воркеров)
Rate limiting 1 (семафор)
Вещание (fan-out) 0

Направленность каналов (Channel Direction)

Go позволяет ограничить направление канала в сигнатуре функции. Это обеспечивает безопасность на уровне типов.

package main

import "fmt"

// producer can ONLY send to channel
func producer(out chan<- int) {
    for i := range 5 {
        out <- i
    }
    close(out)
}

// consumer can ONLY receive from channel
func consumer(in <-chan int) {
    for val := range in {
        fmt.Printf("Got: %d\n", val)
    }
}

func main() {
    ch := make(chan int, 5) // bidirectional

    // Bidirectional channel is implicitly converted to directional
    go producer(ch) // chan int → chan<- int
    consumer(ch)    // chan int → <-chan int
}

Направления:

  • chan T -- двунаправленный (чтение и запись)
  • chan<- T -- только отправка (send-only)
  • <-chan T -- только получение (receive-only)

Правило: Двунаправленный канал можно передать туда, где ожидается однонаправленный. Обратное -- невозможно. Это используется для документирования и обеспечения контракта.

Закрытие каналов

Закрытие канала сигнализирует, что больше никаких значений отправлено не будет:

package main

import "fmt"

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

    // Send values and close
    for i := 1; i <= 5; i++ {
        ch <- i
    }
    close(ch)

    // Method 1: range over channel (stops on close)
    for val := range ch {
        fmt.Println(val)
    }

    // Method 2: comma-ok idiom
    ch2 := make(chan int, 1)
    ch2 <- 42
    close(ch2)

    val, ok := <-ch2
    fmt.Printf("val=%d, ok=%v\n", val, ok) // val=42, ok=true

    val, ok = <-ch2
    fmt.Printf("val=%d, ok=%v\n", val, ok) // val=0, ok=false
}

Правила закрытия каналов:

  • Закрывать должен только отправитель, никогда получатель
  • Закрывать канал можно только один раз (повторное закрытие -- паника)
  • После закрытия чтение возвращает оставшиеся значения, затем нулевое значение типа
  • Запись в закрытый канал -- паника

Аксиомы каналов

Эти правила необходимо знать наизусть:

┌────────────────────────┬──────────────────────┬────────────────────────┐
│ Операция               │ nil канал            │ Закрытый канал         │
├────────────────────────┼──────────────────────┼────────────────────────┤
│ Отправка (ch <- v)     │ Блокирует навсегда   │ ПАНИКА                 │
│ Получение (<-ch)       │ Блокирует навсегда   │ Возвращает zero value  │
│ Закрытие (close(ch))   │ ПАНИКА               │ ПАНИКА                 │
│ len(ch)                │ 0                    │ Количество в буфере    │
│ cap(ch)                │ 0                    │ Ёмкость буфера         │
└────────────────────────┴──────────────────────┴────────────────────────┘
package main

func main() {
    // nil channel — blocks forever
    var ch chan int // nil

    // go func() { ch <- 1 }()  // blocks forever
    // go func() { <-ch }()      // blocks forever
    // close(ch)                  // panic: close of nil channel

    _ = ch
}

Важно: Свойство nil-канала блокировать навсегда активно используется в паттерне с select -- чтобы "отключить" один из case-ов, канал устанавливают в nil.

Оператор select

select позволяет горутине ожидать на нескольких операциях с каналами одновременно. Это мультиплексор для каналов.

package main

import (
    "fmt"
    "time"
)

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

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

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

    // Wait for whichever channel is ready first
    for range 2 {
        select {
        case msg := <-ch1:
            fmt.Println("Received from ch1:", msg)
        case msg := <-ch2:
            fmt.Println("Received from ch2:", msg)
        }
    }
}

select с default (неблокирующий)

package main

import "fmt"

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

    select {
    case val := <-ch:
        fmt.Println("Received:", val)
    default:
        // Executes immediately if no channel is ready
        fmt.Println("No data available")
    }
}

Паттерн timeout

package main

import (
    "fmt"
    "time"
)

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

    go func() {
        time.Sleep(3 * time.Second)
        ch <- 42
    }()

    select {
    case val := <-ch:
        fmt.Println("Received:", val)
    case <-time.After(1 * time.Second):
        fmt.Println("Timeout: no response in 1 second")
    }
}

Правила select

  1. Если несколько case-ов готовы одновременно, выбирается случайный (pseudo-random)
  2. Если ни один case не готов и есть default -- выполняется default
  3. Если ни один case не готов и нет default -- горутина блокируется
  4. select{} без case-ов блокирует навсегда (иногда используется в main)

Обнаружение дедлоков

Рантайм Go обнаруживает ситуации, когда все горутины заблокированы:

package main

func main() {
    ch := make(chan int)
    ch <- 1 // deadlock: main goroutine blocks, no other goroutines exist
}
// fatal error: all goroutines are asleep - deadlock!
package main

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

    // Classic deadlock: A waits for B, B waits for A
    go func() {
        val := <-ch1 // waits for ch1
        ch2 <- val   // never reached
    }()

    val := <-ch2 // waits for ch2
    ch1 <- val   // never reached
}
// fatal error: all goroutines are asleep - deadlock!

Ограничение: Рантайм обнаруживает дедлок только когда все горутины заблокированы. Если хотя бы одна горутина работает (например, HTTP-сервер), дедлок среди других горутин не будет обнаружен.

Паттерн: Producer-Consumer

Классический паттерн "производитель-потребитель" с каналами:

package main

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

// Order represents a customer order
type Order struct {
    ID    int
    Item  string
    Price float64
}

// producer generates orders and sends them to the channel
func producer(orders chan<- Order, count int) {
    items := []string{"Pizza", "Burger", "Sushi", "Tacos", "Pasta"}
    for i := 1; i <= count; i++ {
        order := Order{
            ID:    i,
            Item:  items[i%len(items)],
            Price: float64(i) * 9.99,
        }
        orders <- order
        fmt.Printf("[Producer] Created order #%d: %s\n", order.ID, order.Item)
        time.Sleep(100 * time.Millisecond)
    }
    close(orders) // signal: no more orders
}

// consumer processes orders from the channel
func consumer(id int, orders <-chan Order, wg *sync.WaitGroup) {
    defer wg.Done()
    for order := range orders {
        fmt.Printf("[Consumer %d] Processing order #%d: %s ($%.2f)\n",
            id, order.ID, order.Item, order.Price)
        time.Sleep(250 * time.Millisecond) // simulate processing
    }
    fmt.Printf("[Consumer %d] No more orders, shutting down\n", id)
}

func main() {
    orders := make(chan Order, 5) // buffered channel

    // Start 1 producer
    go producer(orders, 10)

    // Start 3 consumers
    var wg sync.WaitGroup
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go consumer(i, orders, &wg)
    }

    wg.Wait()
    fmt.Println("All orders processed")
}

Паттерн: Пинг-понг (демонстрация синхронности)

package main

import (
    "fmt"
    "time"
)

func main() {
    ping := make(chan struct{})
    pong := make(chan struct{})

    // Player 1
    go func() {
        for range 5 {
            <-ping // wait for ball
            fmt.Println("Player 1: PING")
            time.Sleep(100 * time.Millisecond)
            pong <- struct{}{} // hit back
        }
    }()

    // Player 2
    go func() {
        for range 5 {
            <-pong // wait for ball
            fmt.Println("Player 2: PONG")
            time.Sleep(100 * time.Millisecond)
            ping <- struct{}{} // hit back
        }
    }()

    ping <- struct{}{} // serve the ball
    time.Sleep(2 * time.Second)
}

Идиома: done-канал

package main

import (
    "fmt"
    "time"
)

// doWork runs until done channel is closed
func doWork(done <-chan struct{}) <-chan int {
    results := make(chan int)
    go func() {
        defer close(results)
        i := 0
        for {
            select {
            case <-done:
                fmt.Println("Worker: received done signal")
                return
            case results <- i:
                i++
                time.Sleep(100 * time.Millisecond)
            }
        }
    }()
    return results
}

func main() {
    done := make(chan struct{})
    results := doWork(done)

    // Read 5 results
    for range 5 {
        fmt.Println("Result:", <-results)
    }

    // Signal worker to stop
    close(done)
    time.Sleep(200 * time.Millisecond)
    fmt.Println("Main: done")
}

Проверь себя

Если в select несколько case-ов готовы одновременно, какой будет выбран?

Чем отличается небуферизованный канал от буферизованного с ёмкостью 1?

Какой тип канала нужно использовать для параметра функции, которая только ОТПРАВЛЯЕТ данные?

Что произойдёт при чтении из nil-канала?

Что произойдёт при отправке значения в закрытый канал?

Code Challenges

Ограничитель запросов

GO

Реализуйте Token Bucket Rate Limiter с указанной ёмкостью и скоростью пополнения. Метод Allow() возвращает true, если запрос разрешён.

Test Cases

1. Input: capacity=3, 3 quick calls→ Expected: true,true,true
2. Input: capacity=1, 2 quick calls→ Expected: true,false
3. Input: capacity=1, call, wait, call→ Expected: true,true