Потоковые режимы нужны для подписок, файлов и телеметрии. Все они используют один долгоживущий вызов с несколькими сообщениями.

Серверный стриминг

Клиент подписывается на события, сервер отправляет ответы:

func (s *orderServer) Watch(req *ordersv1.WatchRequest, stream ordersv1.OrderService_WatchServer) error {
    for {
        select {
        case event, ok := <-s.events:
            if !ok {
                return nil
            }
            if err := stream.Send(event); err != nil {
                return err
            }
        case <-stream.Context().Done():
            return stream.Context().Err()
        }
    }
}

Send записывает сообщение в поток. Проверка ok нужна обязательно: закрытый канал событий возвращает нулевое значение бесконечно, поэтому без неё сервер начнёт слать пустые события вместо завершения. При отмене клиента контекст завершается и сервер выходит.

Клиентский стриминг

Клиент отправляет части файла, сервер отвечает один раз после CloseAndRecv:

for _, chunk := range chunks {
    if err := stream.Send(chunk); err != nil {
        return err
    }
}
response, err := stream.CloseAndRecv()

Не забываем закрыть отправку: сервер ждёт конца потока, иначе ответ не появится.

Двунаправленный стриминг

Обе стороны отправляют независимо. Отправка и получение выполняются в разных горутинах, если нужно одновременно читать и писать. Одна общая for { Send; Recv } может заблокировать обмен.

io.EOF означает нормальный конец отправляющей стороны, а не ошибку.

Приём и завершение

На читающей стороне Recv вызывают в цикле до io.EOF или ошибки. io.EOF не превращают в сбой: это нормальное завершение отправителя. Для серверного стрима клиент просто заканчивает чтение, для клиентского сервер после EOF формирует единственный ответ.

клиент сервер chunk 1, chunk 2, ... CloseSend, затем io.EOF один итоговый ответ

При двунаправленном стриме каждую сторону останавливают через контекст вызова. Закрытие канала событий само по себе клиентскую отмену не выполняет.

Коротко

  • Серверный стриминг: один запрос, много ответов.
  • Клиентский: много частей, один итог после закрытия.
  • Двунаправленный требует разделить чтение и запись.
  • Отмена контекста и io.EOF различаются.