Превращаем байты в поток: io.Reader из []byte в Go

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение: Почему вам может понадобиться превратить байты в поток?

Представьте ситуацию: вы написали отличный сервис на Go, который получает данные из внешнего API. Ответ приходит в виде массива байт ([]byte). Всё бы ничего, но вот незадача — следующая функция в вашем пайплайне требует на вход не срез байт, а интерфейс io.Reader. Что делать? Паниковать? Писать велосипед? Нет! Достаточно вспомнить про bytes.NewReader.

Этот вопрос — один из самых популярных на Stack Overflow (тысячи просмотров!). И не зря. В Go концепция io.Reader — это фундамент для работы с потоками данных: файлами, сетевыми соединениями, HTTP-ответами и, конечно, просто байтами в памяти. Если вы начинающий Go-разработчик, рано или поздно вы упрётесь в эту стену. Давайте разберёмся, как её обойти красиво и эффективно.

В этой статье мы не просто дадим сухую команду «используй bytes.NewReader». Мы разберём, почему это работает, какие есть альтернативы, и покажем реальные примеры из жизни. Поехали!

Теория: Что такое io.Reader и с чем его едят?

Прежде чем конвертировать, давайте вспомним, что такое io.Reader. Это интерфейс, определённый в стандартной библиотеке Go:

type Reader interface {
    Read(p []byte) (n int, err error)
}

Всё гениально просто. Любая структура, у которой есть метод Read(p []byte) (int, error), автоматически удовлетворяет интерфейсу io.Reader. Это позволяет писать универсальный код для чтения данных из любого источника: файла, сокета, строки, среза байтов.

Почему это круто? Потому что вы можете написать одну функцию, которая парсит JSON, XML или просто копирует данные, не заботясь о том, откуда они пришли. Вам нужен только io.Reader. Это абстракция потока.

Но что делать, если у вас уже есть данные в памяти (например, []byte)? Вам нужен объект, который «притворится» потоком, но при этом будет читать из вашего среза. Тут на сцену выходит bytes.NewReader.

Основной способ: bytes.NewReader

Это самый простой и правильный путь. Функция bytes.NewReader принимает []byte и возвращает указатель на структуру bytes.Reader, которая реализует io.Reader (а также io.ReadSeeker, io.WriterTo и другие полезные интерфейсы).

Пример 1: Базовое использование

package main

import (
    "bytes"
    "fmt"
    "io"
    "log"
)

func main() {
    // Наш исходный срез байтов
    data := []byte("Привет, мир! Это байты.")

    // Создаём Reader из среза байтов
    reader := bytes.NewReader(data)

    // Читаем данные из reader в буфер
    buf := make([]byte, 10) // читаем по 10 байт
    for {
        n, err := reader.Read(buf)
        if err != nil {
            if err == io.EOF {
                break // закончили чтение
            }
            log.Fatal(err)
        }
        fmt.Printf("Прочитано %d байт: %s\n", n, buf[:n])
    }
    // Output:
    // Прочитано 10 байт: Привет, ми
    // Прочитано 10 байт: р! Это бай
    // Прочитано 6 байт: ты.
}

Обратите внимание: bytes.Reader ведёт себя как настоящий поток. Когда данные заканчиваются, он возвращает io.EOF. Всё честно.

Пример 2: Использование с io.Copy

Самый частый сценарий — скопировать байты куда-то (в файл, в HTTP-ответ). Для этого идеально подходит io.Copy:

package main

import (
    "bytes"
    "fmt"
    "io"
    "os"
)

func main() {
    data := []byte("Сохраняем эти данные в файл.")
    reader := bytes.NewReader(data)

    // Создаём файл
    file, err := os.Create("output.txt")
    if err != nil {
        panic(err)
    }
    defer file.Close()

    // Копируем из reader в файл
    written, err := io.Copy(file, reader)
    if err != nil {
        panic(err)
    }
    fmt.Printf("Успешно записано %d байт в файл.\n", written)
}

Этот код создаст файл output.txt и запишет туда нашу строку. io.Copy сам позаботится о буферизации и чтении до io.EOF.

Альтернативы: Когда bytes.NewReader не подходит?

Хотя bytes.NewReader — король в этой ситуации, есть и другие способы. Давайте посмотрим на них, чтобы вы знали полную картину.

1. strings.NewReader (для строк)

Если ваши данные — это строка (string), а не срез байтов, можно использовать strings.NewReader. Он работает аналогично, но принимает string:

package main

import (
    "fmt"
    "io"
    "strings"
)

func main() {
    str := "Привет из строки!"
    reader := strings.NewReader(str)

    // Читаем всё сразу
    result, _ := io.ReadAll(reader)
    fmt.Println(string(result)) // Привет из строки!
}

Под капотом strings.NewReader тоже использует байты (так как строка в Go — это неизменяемый срез байт). Но если у вас уже есть строка, конвертировать её в []byte только для того, чтобы передать в bytes.NewReader, — лишняя работа. Используйте strings.NewReader.

2. io.MultiReader (объединение нескольких reader'ов)

Иногда нужно объединить несколько срезов байтов в один поток. Для этого есть io.MultiReader:

package main

import (
    "bytes"
    "fmt"
    "io"
)

func main() {
    part1 := []byte("Первая часть. ")
    part2 := []byte("Вторая часть.")
    
    r1 := bytes.NewReader(part1)
    r2 := bytes.NewReader(part2)
    
    // Объединяем
    multiReader := io.MultiReader(r1, r2)
    
    result, _ := io.ReadAll(multiReader)
    fmt.Println(string(result)) // Первая часть. Вторая часть.
}

3. Самописный Reader (для экстремалов)

Если вам нужно какое-то особенное поведение (например, проверять контрольную сумму при чтении), вы можете реализовать свой собственный Reader. Но для 99% задач это избыточно. Вот минимальный пример, просто чтобы вы знали, как это делается:

package main

import (
    "fmt"
    "io"
)

type MyReader struct {
    data []byte
    pos  int
}

func (r *MyReader) Read(p []byte) (n int, err error) {
    if r.pos >= len(r.data) {
        return 0, io                

Блоги

Рекомендаций по книгам