Привет, Хабр! Сегодня мы рассмотрим замечательный пакет в Golang bufio. Пакет bufio — это стандартная библиотека Go, предназначенная для буферизации ввода-вывода. Почему буферизация важна? Представьте, что вы пытаетесь читать или записывать данные по одному байту за раз. Это утомительно и неэффективно. bufio помогает объединить множество мелких операций в более крупные блоки.

Пакет bufio имеет несколько основных структур и методов:

  • bufio.Reader — буферизованный ридер для чтения данных из io.Reader. Создается с помощью функции bufio.NewReader(r io.Reader) *bufio.Reader.

  • bufio.Writer — буферизованный писатель для записи данных в io.Writer. Создается через bufio.NewWriter(w io.Writer) *bufio.Writer. Он накапливает данные в буфере перед записью.

  • bufio.Scanner — удобный инструмент для построчного или токенизированного чтения данных. Создается с помощью bufio.NewScanner(r io.Reader) *bufio.Scanner. Его часто юзают для простых задач по чтению данных, таких как парсинг файлов построчно.

  • bufio.ReadWriter — комбинация bufio.Reader и bufio.Writer, позволяющая одновременно читать и писать данные. Создается через bufio.NewReadWriter(r *bufio.Reader, w *bufio.Writer) *bufio.ReadWriter.

Каждая из этих структур обладает набором методов.

Методы bufio.Reader

bufio.Reader имеет ряд методов для чтения данных:

  • Read(p []byte) (n int, err error) — читает до len(p) байт в p.

  • ReadByte() (byte, error) — читает один байт.

  • ReadBytes(delim byte) ([]byte, error) — читает до delim байта включительно.

  • ReadString(delim byte) (string, error) — аналогично ReadBytes, но возвращает строку.

  • Peek(n int) ([]byte, error) — позволяет взглянуть на следующие n байт без их чтения.

Методы bufio.Writer

bufio.Writer также имеет несколько полезных методов:

  • Write(p []byte) (n int, err error) — записывает p в буфер.

  • WriteString(s string) (n int, err error) — записывает строку в буфер.

  • Flush() error — сбрасывает буфер, записывая его содержимое в io.Writer.

Особенно важен метод Flush! Он гарантирует, что все данные, накопленные в буфере, будут записаны в целевой источник. Без вызова Flush данные могут остаться в буфере и никогда не попасть в файл или на экран.

Методы bufio.Scanner

bufio.Scanner предназначен для удобного и простого чтения данных:

  • Scan() bool — читает следующий токен.

  • Text() string — возвращает текст последнего токена.

  • Bytes() []byte — возвращает байты последнего токена.

  • Err() error — возвращает ошибку, если она произошла.

Методы bufio.ReadWriter

Комбинированная структура bufio.ReadWriter имеет методы для одновременного чтения и записи:

  • ReadString(delim byte) (string, error) — читает строку до delim.

  • WriteString(s string) (int, error) — записывает строку в буфер.

  • Flush() error — сбрасывает буфер.

Применение

Чтение файла построчно с bufio.Reader

Допустим нужно прочитать файл data.txt построчно и обработать каждую строку. Вот как это можно сделать с использованием bufio.Reader:

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("data.txt")
    if err != nil {
        fmt.Printf("Ошибка открытия файла: %v\n", err)
        return
    }
    defer func() {
        if err := file.Close(); err != nil {
            fmt.Printf("Ошибка закрытия файла: %v\n", err)
        }
    }()

    reader := bufio.NewReader(file)
    for {
        line, err := reader.ReadString('\n')
        if err != nil {
            if err.Error() != "EOF" {
                fmt.Printf("Ошибка чтения файла: %v\n", err)
            }
            break
        }
        processLine(line)
    }
}

func processLine(line string) {
    fmt.Print(line) // Здесь можно добавить любую обработку строки
}

Открываем файл с помощью os.Open и гарантируем его закрытие с помощью defer. Затем создаем буферизованный ридер с помощью bufio.NewReader, что повышает эффективность чтения. В бесконечном цикле читаем строки до символа \n и обрабатываем их функцией processLine.

Буферизованная запись в файл с bufio.Writer

Запись данных в файл также может быть оптимизирована с помощью буферизации. Рассмотрим пример, где записываем 1000 строк в output.txt:

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    file, err := os.Create("output.txt")
    if err != nil {
        fmt.Printf("Ошибка создания файла: %v\n", err)
        return
    }
    defer func() {
        if err := file.Close(); err != nil {
            fmt.Printf("Ошибка закрытия файла: %v\n", err)
        }
    }()

    writer := bufio.NewWriter(file)
    for i := 1; i <= 1000; i++ {
        _, err := writer.WriteString(fmt.Sprintf("Строка номер %d\n", i))
        if err != nil {
            fmt.Printf("Ошибка записи: %v\n", err)
            return
        }
    }

    if err := writer.Flush(); err != nil {
        fmt.Printf("Ошибка сброса буфера: %v\n", err)
    }
}

bufio.Writer накапливает данные в буфере, уменьшая количество операций записи. Не забудьте вызвать Flush после записи, чтобы данные попали в файл. Без этого данные могут "застрять" в буфере, как кофе в забытом стакане.

bufio.Scanner для простого чтения

Если нужно быстро и просто читать файл построчно без сложной обработки, то здесь хорошо зайдетbufio.Scanner.

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("scan_example.txt")
    if err != nil {
        fmt.Printf("Ошибка открытия файла: %v\n", err)
        return
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    lineNumber := 1
    for scanner.Scan() {
        fmt.Printf("Строка %d: %s\n", lineNumber, scanner.Text())
        lineNumber++
    }

    if err := scanner.Err(); err != nil {
        fmt.Printf("Ошибка сканирования: %v\n", err)
    }
}

Комбинированное чтение и запись с bufio.ReadWriter

Иногда возникает необходимость одновременно читать и записывать данные. Например, нужно читать входящие сообщения из файла и записывать обработанные данные обратно. Для этого хорошо подходит bufio.ReadWriter:

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    file, err := os.OpenFile("readwriter.txt", os.O_RDWR|os.O_CREATE, 0644)
    if err != nil {
        fmt.Printf("Ошибка открытия файла: %v\n", err)
        return
    }
    defer func() {
        if err := file.Close(); err != nil {
            fmt.Printf("Ошибка закрытия файла: %v\n", err)
        }
    }()

    rw := bufio.NewReadWriter(bufio.NewReader(file), bufio.NewWriter(file))

    // Чтение первой строки
    line, err := rw.ReadString('\n')
    if err != nil {
        fmt.Printf("Ошибка чтения: %v\n", err)
        return
    }
    fmt.Printf("Прочитано: %s", line)

    // Переход в конец файла для записи
    _, err = rw.WriteString("Добавленная строка\n")
    if err != nil {
        fmt.Printf("Ошибка записи: %v\n", err)
        return
    }

    // Сброс буфера
    if err := rw.Flush(); err != nil {
        fmt.Printf("Ошибка сброса буфера: %v\n", err)
    }
}

Прочие техники

По дефолту Scanner разделяет ввод по строкам. Но что, если вам нужно разделить ввод по словам или по произвольным токенам? Легко:

scanner := bufio.NewScanner(file)
scanner.Split(bufio.ScanWords)
for scanner.Scan() {
    word := scanner.Text()
    fmt.Println(word)
}

А если нужно разделять ввод по символу ,:

scanner := bufio.NewScanner(file)
scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {
    for i, b := range data {
        if b == ',' {
            return i + 1, data[:i], nil
        }
    }
    if atEOF && len(data) > 0 {
        return len(data), data, nil
    }
    return 0, nil, nil
})
for scanner.Scan() {
    token := scanner.Text()
    fmt.Println(token)
}

Иногда данные слишком большие для стандартного буфера. Можно увеличить размер буфера Scanner следующим образом:

scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 1024*1024) // 1MB
scanner.Buffer(buf, 1024*1024)

Метод Peek позволяет взглянуть на следующие n байт без их чтения:

reader := bufio.NewReader(file)
peekBytes, err := reader.Peek(5)
if err != nil {
    fmt.Printf("Ошибка Peek: %v\n", err)
    return
}
fmt.Printf("Первые 5 байт: %s\n", string(peekBytes))

Советы

После записи данных всегда вызывайте Flush. Это гарантирует, что все данные попадут в целевой источник. Забудете — и ваши данные останутся в буфере:

if err := writer.Flush(); err != nil {
    fmt.Printf("Ошибка сброса буфера: %v\n", err)
}

Всегда проверяйте ошибки после операций чтения и записи:

line, err := reader.ReadString('\n')
if err != nil && err != io.EOF {
    // Обработка ошибки
}

bufio не является потокобезопасным. Поэтому если вы используете его в многопоточной среде, нужно сделать синхронизацию через мьютексы или другие механизмы.

Используйте defer мудро: закрывайте файлы и сбрасывайте буферы с помощью defer:

defer func() {
    if err := writer.Flush(); err != nil {
        log.Fatalf("Ошибка сброса буфера: %v", err)
    }
    if err := file.Close(); err != nil {
        log.Fatalf("Ошибка закрытия файла: %v", err)
    }
}()

Комбинируйте с другими пакетами: bufio отлично сочетается с io, os, fmt и другими пакетами.


Если у вас есть интересные примеры использования bufio — отправляйте их в комментарии!

Изучить Go — от основ и внутреннего устройства до создания микросервисов и взаимодействия с другими системами — можно на онлайн-курсе "Golang Developer. Professional". В рамках курса в январе пройдут открытые уроки, на которые приглашаем всех желающих. Записаться можно на странице курса по ссылкам ниже:

  • 13 января: Составляем индивидуальный план развития Go-инженера от Junior до Middle. Подробнее

  • 22 января: Кошелек или жизнь? Фича или баг? Хелсчеки в k8s. Подробнее

Комментарии (0)