Golang: bytes.Buffer изнутри
- среда, 10 июля 2024 г. в 00:00:06
Работая с кодом на Go, любому специалисту приходилось сталкиваться со стандартным пакетом bytes
. Внутри него лежит определение Buffer
. Что же это такое?
Сам по себе bytes.Buffer
является структурой.
type Buffer struct {
buf []byte // содержимое - это байты buf[off : len(buf)]
off int // читает по &buf[off], пишет по &buf[len(buf)]
lastRead readOp // последняя операция чтения, чтобы Unread* могло работать корректно
}
Buffer — это буфер с переменным размером, который может быть использован для чтения и записи данных. При инициализации буфера с нулевым значением, он будет пустым и готовым, чтобы его использовали.
Поля структуры Buffer
в Go используются для управления внутренним состоянием буфера при чтении и записи данных.
Давайте разберем каждое поле в структуре:
buf
- хранит данные буфера в виде среза байтов. Все операции чтения и записи происходят с использованием этого среза
off
- определяет текущее смещение для чтения данных из буфер. Чтение данных происходит с позиции buf[off]
. Позиция off
увеличивается по мере чтения данных. Запись данных всегда происходит в конец буфера по адресу &buf[len(buf)]
lastRead
- хранит тип последней операции чтения (например, Read
, UnreadByte
, и т.д.), что позволяет методам Unread*
(например, UnreadByte
, UnreadRune
) корректно выполнять свои функции. Это поле используется для контроля операций отмены последнего чтения, чтобы вернуть прочитанные данные обратно в буфер.
Так, хорошо, мы разобрались какие поля для чего нужны, но нужно бы посмотреть определение readOp
.
type readOp int8
Мы видим, что по сути оно является int8
типом. Дальше заглянем в описание этого типа.
The
readOp
constants describe the last action performed on the buffer, so thatUnreadRune
andUnreadByte
can check for invalid usage.opReadRuneX
constants are chosen such that converted to int they correspond to the rune size that was read.
Что в переводе:
Константы
readOp
описывают последнее действие, выполненное с буфером, поэтомуUnreadRune
иUnreadByte
могут проверять наличие недопустимого использования. КонстантыopReadRuneX
выбраны таким образом, чтобы в преобразовании вint
они соответствовали размеру руны, который был прочитан.
Посмотрим инициализацию readOp
.
const (
opRead readOp = -1 // Любая другая операция на чтение
opInvalid readOp = 0 // Операция не на чтение
opReadRune1 readOp = 1 // Читающая руна размером 1
opReadRune2 readOp = 2 // Читающая руна размером 2
opReadRune3 readOp = 3 // Читающая руна размером 3
opReadRune4 readOp = 4 // Читающая руна размером 4
)
Хорошо, с readOp
разобрались и какие она значения принимает.
Разберемся с методами буфера.
func (b *Buffer) Bytes() []byte { return b.buf[b.off:] }
Метод Bytes
в структуре Buffer
возвращает срез, содержащий непрочитанные данные из буфера. Давайте подробнее рассмотрим, что он делает и для чего он нужен.
Метод используется для получения среза байтов, представляющего все данные, которые еще не были прочитаны из буфера. Это позволяет легко получить доступ к оставшемуся содержимому буфера без необходимости дополнительного копирования данных.
Метод возвращает срез b.buf[b.off:]
, который начинается с текущего смещения b.off
и идет до конца буфера. Таким образом, он возвращает все данные, которые еще не были прочитаны.
Пример использования:
func TryBytes() {
data := []byte("Hello, world!")
buffer := bytes.NewBuffer(data)
// создаем срез нужного размера для чтения части данных
p := make([]byte, 5)
n, err := buffer.Read(p)
if err != nil {
fmt.Println("Error reading buffer:", err)
return
}
fmt.Printf("Read %d bytes: %s\n", n, p[:n])
// Read 5 bytes: Hello
// получаем оставшиеся данные в буфере
remaining := buffer.Bytes()
fmt.Printf("Remaining buffer data: %s\n", remaining)
// Remaining buffer data: , world!
}
Метод Bytes
эффективен, потому что он возвращает срез без дополнительных аллокаций или копирований данных. Он просто предоставляет доступ к уже существующим данным, что делает его быстрым и экономичным по памяти.
Важные моменты:
Непрочитанные данные: Метод возвращает только непрочитанные данные, начиная с текущего смещения b.off
Изменения буфера: Любые изменения в исходном буфере будут отражены в срезе, возвращенном методом Bytes
, так как это тот же самый участок памяти
func (b *Buffer) AvailableBuffer() []byte { return b.buf[len(b.buf):] }
Метод AvailableBuffer
в структуре Buffer
возвращает пустой срез с ёмкостью, равной доступному пространству в буфере. Этот срез предназначен для последующего добавления данных и передачи в вызов метода Buffer.Write
. Буфер действителен только до следующей операции записи в b
.
AvailableBuffer
предназначен для предоставления доступа к свободному пространству в буфере. Он возвращает пустой срез, который начинается с конца текущих данных в буфере. Этот срез может быть использован для добавления новых данных перед следующим вызовом Buffer.Write
.
Пример:
func TryAvailableBuffer() {
// инициализация буфера с некоторыми данными
data := []byte("Hello, world!")
buffer := bytes.NewBuffer(data)
// получение пустого среза для добавления новых данных
available := buffer.AvailableBuffer()
fmt.Printf("Available buffer capacity: %d\n", cap(available))
// Available buffer capacity: 0
// запись новых данных в буфер
newData := []byte(" New data")
buffer.Write(newData)
// чтение всех данных из буфера после записи
remaining := buffer.Bytes()
fmt.Printf("Remaining buffer data: %s\n", remaining)
// Remaining buffer data: Hello, world! New data
}
Тут все просто, он возвращает содержимое непрочитанной части буфера в виде строки. Если указатель на Buffer
является nil, то метод возвращает строку "<nil>"
. Этот метод полезен для получения строкового представления данных, находящихся в буфере, и может быть полезен при отладке.
func (b *Buffer) String() string {
if b == nil {
// специальный случай, полезен в отладке
return "<nil>"
}
return string(b.buf[b.off:])
}
Пример:
func TryString() {
// инициализация буфера с некоторыми данными
data := []byte("Hello, world!")
buffer := bytes.NewBuffer(data)
// получение строкового представления буфера
fmt.Println("Buffer contents:", buffer.String())
// Buffer contents: Hello, world!
// запись новых данных в буфер
newData := []byte(" New data")
buffer.Write(newData)
// получение строкового представления буфера после записи
fmt.Println("Buffer contents after write:", buffer.String())
// Buffer contents after write: Hello, world! New data
}
Метод empty
используется для определения того, пуст ли буфер, с точки зрения чтения данных. Возвращаемое значение true
указывает на то, что непрочитанная часть буфера исчерпана (то есть все данные уже прочитаны или буфер не содержит данных вообще), а false
означает, что в буфере еще есть данные, которые могут быть прочитаны.
func (b *Buffer) empty() bool { return len(b.buf) <= b.off }
Он используется в других методах, которые мы затронем в дальнейшем.
Метод Len
возвращает количество байтов непрочитанной части буфера.
func (b *Buffer) Len() int { return len(b.buf) - b.off }
Пример использования:
func TryLen() {
// инициализация буфера с некоторыми данными
data := []byte("Hello, world!")
buffer := bytes.NewBuffer(data)
// получение длины непрочитанной части буфера с использованием метода Len
length := buffer.Len()
fmt.Printf("Length of buffer: %d\n", length)
// Length of buffer: 13
}
Метод Cap
используется для получения ёмкости (capacity) буфера, что представляет собой общее количество байтов, которое может вместить подложный срез байтов b.buf
. Ёмкость определяет, сколько данных может содержаться в буфере до того, как потребуется выделение дополнительной памяти для увеличения размера среза.
func (b *Buffer) Cap() int { return cap(b.buf) }
func TryCap() {
// инициализация буфера с некоторыми данными
data := []byte("Hello, world!")
buffer := bytes.NewBuffer(data)
// получение ёмкости буфера
capacity := buffer.Cap()
fmt.Printf("Capacity of buffer: %d\n", capacity)
// Capacity of buffer: 13
// чтение данных из буфера (для демонстрации, не влияет на ёмкость)
p := make([]byte, 5)
buffer.Read(p)
// повторное получение ёмкости (не должна измениться)
fmt.Printf("Capacity of buffer after reading: %d\n", buffer.Cap())
// Capacity of buffer after reading: 13
}
func (b *Buffer) Available() int { return cap(b.buf) - len(b.buf) }
Available
используется для определения количества байтов, которые еще можно добавить в буфер без выделения дополнительной памяти.
func TryAvailable() {
// инициализация буфера с некоторыми данными
data := []byte("Hello, world!")
buffer := bytes.NewBuffer(data)
// получение доступного пространства в буфере до его заполнения
available := buffer.Available()
fmt.Printf("Available bytes in buffer before write: %d\n", available)
// Available bytes in buffer before write: 0
// добавление данных в буфер
additionalData := []byte(" Additional data.")
n, err := buffer.Write(additionalData)
if err != nil {
fmt.Println("Error writing to buffer:", err)
return
}
fmt.Printf("Wrote %d bytes to buffer\n", n)
// Wrote 17 bytes to buffer
// повторное получение доступного пространства в буфере
available = buffer.Available()
fmt.Printf("Available bytes in buffer after write: %d\n", available)
// Available bytes in buffer after write: 2
}
Объяснение:
Инициализация буфера: Буфер buffer
создается с данными "Hello, world!". В начале Available
показывает 0
, так как буфер заполнен до своей ёмкости (13 байтов).
Добавление данных: Мы записываем строку " Additional data." в буфер. Длина этой строки составляет 17 байтов. После записи буфер не расширяется автоматически, и доступные для записи байты остаются.
Повторное получение доступного пространства: После записи Available
показывает 2
, так как после записи строки " Additional data." в буфере осталось 2 байта свободного пространства.
func (b *Buffer) Truncate(n int) {
if n == 0 {
// cброс буфера при n == 0: Если n равно 0, метод вызывает Reset(),
// который сбрасывает буфер, устанавливая его в начальное состояние без данных.
b.Reset()
return
}
b.lastRead = opInvalid
// простая валидация
if n < 0 || n > b.Len() {
panic("bytes.Buffer: truncation out of range")
}
// если все проверки успешны, то метод обрезает срез b.buf до
// длины b.off + n. Это означает, что буфер будет содержать только
// первые n непрочитанных байтов, остальная часть данных будет удалена из буфера.
b.buf = b.buf[:b.off+n]
}
Метод Truncate
предназначен для обрезки буфера до указанной длины n
, оставляя только первые n
непрочитанных байтов.
Пример:
func TryTruncate() {
// инициализация буфера с некоторыми данными
data := []byte("Hello, world!")
buffer := bytes.NewBuffer(data)
// вывод начального содержимого буфера
fmt.Printf("Buffer before Truncate: %s\n", buffer.Bytes())
// Buffer before Truncate: Hello, world!
// обрезка буфера до первых 5 непрочитанных байтов
buffer.Truncate(5)
// вывод содержимого буфера после обрезки
fmt.Printf("Buffer after Truncate: %s\n", buffer.Bytes())
// Buffer after Truncate: Hello
}
func (b *Buffer) Reset() {
b.buf = b.buf[:0] // обрезаем срез buf до длины 0, тем самым очищаем содержимое
b.off = 0 // сбрасываем смещение чтения/записи
b.lastRead = opInvalid // устанавливаем последнюю операцию чтения в
// неопределенное состояние про которое рассказывалось в начале статьи
}
Метод Reset
используется для сброса содержимого буфера, делая его пустым, но при этом сохраняя выделенную для него память для будущих записей. Он полезен, когда вам нужно использовать буфер повторно, сохраняя при этом выделенную память для эффективного управления ресурсами. Также позволяет быстро очистить содержимое буфера без необходимости повторного выделения памяти. Пример:
func TryReset() {
// инициализация буфера с некоторыми данными
data := []byte("Hello, world!")
buffer := bytes.NewBuffer(data)
// вывод содержимого буфера до сброса
fmt.Printf("Buffer before Reset: %s\n", buffer.Bytes())
// Buffer before Reset: Hello, world!
// сброс буфера
buffer.Reset()
// вывод содержимого буфера после сброса
fmt.Printf("Buffer after Reset: %s\n", buffer.Bytes())
// Buffer after Reset:
}
func (b *Buffer) Grow(n int) {
if n < 0 {
panic("bytes.Buffer.Grow: negative count")
}
m := b.grow(n)
b.buf = b.buf[:m] // обрезаем буфер до новой емкости
}
Он используется для увеличения ёмкости буфера так, чтобы было гарантировано достаточное пространство для записи ещё n
байт. Он будет полезен, когда вы заранее знаете, сколько данных вы собираетесь записать в буфер, и хотите избежать повторных выделений памяти при каждой операции записи. Также посмотрим определение внутреннего метода grow
:
func (b *Buffer) grow(n int) int {
m := b.Len()
// если буфер пустой, мы сбрасываем его, чтобы освободить пространство
if m == 0 && b.off != 0 {
b.Reset()
}
// пытаемся вырасти с помощью внутреннего метода
if i, ok := b.tryGrowByReslice(n); ok {
return i
}
if b.buf == nil && n <= smallBufferSize {
b.buf = make([]byte, n, smallBufferSize)
return 0
}
c := cap(b.buf)
if n <= c/2-m {
// мы можем перемещать объекты вниз вместо выделения нового фрагмента
// Для перемещения нам нужно всего лишь m+n <= c, но
// вместо этого мы увеличиваем объем в два раза, чтобы
// не тратить все время на копирование
copy(b.buf, b.buf[b.off:])
} else if c > maxInt-c-n {
panic(ErrTooLarge)
} else {
// добавляем b.off для учета b.buf[:b.off] быть срезанным спереди
b.buf = growSlice(b.buf[b.off:], b.off+n)
}
// очищаем b.off и len(b.buf).
b.off = 0
b.buf = b.buf[:m+n]
return m
}
Также взглянем на определение tryGrowByReslice
:
func (b *Buffer) tryGrowByReslice(n int) (int, bool) {
if l := len(b.buf); n <= cap(b.buf)-l {
b.buf = b.buf[:l+n]
return l, true
}
return 0, false
}
Она используется для быстрого увеличения, когда внутренний буфер требует только перезаписи и возращает индекс в который должны быть записаны байты и удалось ли это выполнить.
Пример:
func TryGrow {
// инициализация буфера с некоторыми данными
data := []byte("Hello, world!")
buffer := bytes.NewBuffer(data)
// вывод текущей ёмкости буфера
fmt.Printf("Buffer capacity before Grow: %d\n", buffer.Cap())
// Buffer capacity before Grow: 13
// увеличение ёмкости буфера для записи ещё 50 байт
buffer.Grow(50)
// вывод новой ёмкости буфера после увеличения
fmt.Printf("Buffer capacity after Grow: %d\n", buffer.Cap())
// Buffer capacity after Grow: 64
}
func (b *Buffer) Write(p []byte) (n int, err error) {
b.lastRead = opInvalid
// попытка увеличить ёмкость буфера без выделения новой памяти
m, ok := b.tryGrowByReslice(len(p))
// если попытка не удалась, вызываем метод grow для увеличения ёмкости
if !ok {
m = b.grow(len(p))
}
// копируем содержимое p в буфер и возвращаем длину p и nil в качестве ошибки
return copy(b.buf[m:], p), nil
}
Тут можно обойтись без примера, так как выше в статье использовался этот метод.
func (b *Buffer) WriteString(s string) (n int, err error) {
b.lastRead = opInvalid
// попытка увеличить ёмкость буфера без выделения новой памяти
m, ok := b.tryGrowByReslice(len(s))
//еЕсли попытка не удалась, вызываем метод grow для увеличения ёмкости
if !ok {
m = b.grow(len(s))
}
// копируем содержимое s в буфер и возвращаем длину s и nil в качестве ошибки
return copy(b.buf[m:], s), nil
}
Метод WriteString
аналогичен методу Write
, но оптимизирован для работы со строками. Он предоставляет удобный способ добавления содержимого строки в буфер.
func (b *Buffer) ReadFrom(r io.Reader) (n int64, err error) {
b.lastRead = opInvalid
for {
// увеличение ёмкости буфера на минимальное количество байт (MinRead)
i := b.grow(MinRead)
b.buf = b.buf[:i]
// чтение данных из r в буфер, начиная с позиции i и до ёмкости буфера
m, e := r.Read(b.buf[i:cap(b.buf)])
// паника, если результат чтения m < 0 (неожиданное отрицательное значение)
if m < 0 {
panic(errNegativeRead)
}
// установка новой длины буфера после чтения
b.buf = b.buf[:i+m]
n += int64(m)
// если встретился конец файла (EOF), возвращаем количество прочитанных байт и nil
if e == io.EOF {
return n, nil
}
// если произошла ошибка, возвращаем количество прочитанных байт и ошибку
if e != nil {
return n, e
}
}
}
Метод предназначен для чтения данных из io.Reader
до EOF и добавления их в буфер, увеличивая его размер при необходимости. Также стоит отметить определение размера const MinRead = 512
, MinRead
- это минимальный размер фрагмента, передаваемый в вызов Read
с помощью Buffer.ReadFrom
. Если в буфере есть байты MinRead
, превышающие то, что требуется для хранения содержимого r
, ReadFrom
не приведет к увеличению базового буфера. Что достаточно полезно.
ReadFrom
эффективен для чтения данных из io.Reader
и добавления их в буфер без необходимости создания промежуточного буфера. Он динамически увеличивает ёмкость буфера, чтобы поместить все данные.
Пример:
func TryReadForm() {
// создаем буфер для записи данных
var buf bytes.Buffer
// создаем строку, которую будем записывать в буфер
data := "Hello, world!\nThis is a test string."
// используем strings.NewReader для создания io.Reader из строки data
reader := strings.NewReader(data)
// используем метод ReadFrom для чтения данных из reader и записи в буфер
n, err := buf.ReadFrom(reader)
if err != nil {
fmt.Println("Error reading from reader:", err)
return
}
// выводим результаты чтения
fmt.Printf("Read %d bytes from reader and wrote to buffer\n", n)
// Read 36 bytes from reader and wrote to buffer
// выводим содержимое буфера
fmt.Println("Buffer contents:")
// Buffer contents:
fmt.Println(buf.String())
// Hello, world!
// This is a test string.
}
func (b *Buffer) WriteTo(w io.Writer) (n int64, err error) {
b.lastRead = opInvalid
// получаем количество байт в непрочитанной части буфера
if nBytes := b.Len(); nBytes > 0 {
// записываем данные из буфера в io.Writer w
m, e := w.Write(b.buf[b.off:])
// проверяем, что количество записанных байт m не больше количества байт в буфере
if m > nBytes {
panic("bytes.Buffer.WriteTo: invalid Write count")
}
// обновляем смещение в буфере
b.off += m
n = int64(m)
// если произошла ошибка при записи, возвращаем количество байт и ошибку
if e != nil {
return n, e
}
// по определению метода Write в io.Writer, все байты должны быть записаны
// если записанное количество байт m не соответствует nBytes, возвращаем ошибку io.ErrShortWrite
if m != nBytes {
return n, io.ErrShortWrite
}
}
// буфер теперь пуст, сбрасываем его.
b.Reset()
return n, nil
}
Этот метод полезен, когда требуется передать содержимое буфера непосредственно в другой io.Writer
, такой как файл или сетевое соединение.
Пример использования:
func TryWriteTo() {
// создаем буфер для записи данных
var buf bytes.Buffer
// записываем данные в буфер
buf.WriteString("Hello, world!\n")
buf.WriteString("This is a test string.")
// создаем файл для записи
file, err := os.Create("output.txt")
if err != nil {
fmt.Println("Error creating file:", err)
return
}
defer file.Close()
// используем метод WriteTo для записи содержимого буфера в файл
n, err := buf.WriteTo(file)
if err != nil {
fmt.Println("Error writing to file:", err)
return
}
// Выводим результаты записи
fmt.Printf("Wrote %d bytes to file\n", n)
// Wrote 36 bytes to file
}
func (b *Buffer) WriteByte(c byte) error {
b.lastRead = opInvalid
// пытаемся увеличить размер буфера путем ресайза
m, ok := b.tryGrowByReslice(1)
if !ok {
// если ресайз не удался, выделяем новый участок памяти
m = b.grow(1)
}
// добавляем байт c в буфер
b.buf[m] = c
// возвращаем nil, так как ошибка всегда равна nil
return nil
}
Метод WriteByte
полезен для добавления отдельных байтов в конец буфера. Он автоматически увеличивает размер буфера при необходимости, что обеспечивает эффективное добавление данных. Пример использваония:
func TryWriteByte() {
// создаем новый буфер
var buf bytes.Buffer
// записываем несколько байтов в буфер с помощью WriteByte
buf.WriteByte('H')
buf.WriteByte('e')
buf.WriteByte('l')
buf.WriteByte('l')
buf.WriteByte('o')
// получаем содержимое буфера как строку
fmt.Println("Buffer contents:", buf.String()) // Выводит: Buffer contents: Hello
// Buffer contents: Hello
}
func (b *Buffer) WriteRune(r rune) (n int, err error) {
// сравниваем как uint32 для правильной обработки отрицательных рун.
if uint32(r) < utf8.RuneSelf {
b.WriteByte(byte(r))
return 1, nil
}
b.lastRead = opInvalid
// пытаемся ресайзнуть
m, ok := b.tryGrowByReslice(utf8.UTFMax)
if !ok {
m = b.grow(utf8.UTFMax)
}
b.buf = utf8.AppendRune(b.buf[:m], r)
return len(b.buf) - m, nil
}
Ну для чего он по предыдущим примерам понятно, поэтому можно обойтись без примера.
func (b *Buffer) Read(p []byte) (n int, err error) {
b.lastRead = opInvalid
// Если буфер пуст, сбросим его
if b.empty() {
b.Reset()
// Если длина p равна 0, вернем 0 и nil
if len(p) == 0 {
return 0, nil
}
// Если длина p не равна 0, возвращаем 0 и io.EOF
return 0, io.EOF
}
// Копируем данные из буфера в p
n = copy(p, b.buf[b.off:])
b.off += n
// Если были скопированы данные, устанавливаем lastRead на opRead
if n > 0 {
b.lastRead = opRead
}
return n, nil
}
Предназначен для чтения данных из буфера в заданный срез.
func (b *Buffer) Next(n int) []byte {
b.lastRead = opInvalid
// Вычисляем текущую длину непрочитанной части буфера
m := b.Len()
// Если запрашиваемых байт больше, чем доступно в буфере, устанавливаем n равным m
if n > m {
n = m
}
// Создаем срез данных из буфера, начиная с текущего смещения b.off и длиной n
data := b.buf[b.off : b.off+n]
// Перемещаем указатель b.off на n байт вперед
b.off += n
// Если были скопированы данные, устанавливаем lastRead на opRead
if n > 0 {
b.lastRead = opRead
}
return data
}
Возвращает срез, содержащий следующие n
байт из буфера, продвигая указатель чтения (b.off
) так, как если бы эти байты были прочитаны с помощью метода Buffer.Read
. Этот метод особенно полезен для эффективного чтения последовательных блоков данных из буфера, например, при обработке потока данных или парсинге сообщений.
Пример:
func TryNext() {
// Создаем новый буфер с данными
data := []byte("Hello, world!")
buffer := bytes.NewBuffer(data)
// Читаем первые 5 байт из буфера с помощью метода Next
firstFive := buffer.Next(5)
fmt.Printf("First five bytes: %s\n", firstFive)
// First five bytes: Hello
// Читаем следующие 10 байт из буфера (по факту, останется только "orld!")
nextTen := buffer.Next(10)
fmt.Printf("Next ten bytes: %s\n", nextTen)
// Next ten bytes: , world!
// После вызова Next буфер сместился на соответствующее количество байт
// и больше не содержит первых прочитанных данных.
}
func (b *Buffer) ReadByte() (byte, error) {
// Если буфер пуст, сбрасываем его
if b.empty() {
b.Reset()
return 0, io.EOF
}
// Читаем байт из текущей позиции b.off
c := b.buf[b.off]
// Увеличиваем смещение b.off для следующего чтения
b.off++
// Устанавливаем lastRead на opRead, т.к. был успешный вызов чтения
b.lastRead = opRead
// Возвращаем прочитанный байт и nil в качестве ошибки
return c, nil
}
Этот метод подходит для ситуаций, когда вам нужно последовательно обрабатывать данные по одному байту из буфера, например, при разборе структурированных данных или при чтении текстовых файлов. После успешного вызова ReadByte
, смещение b.off
увеличивается на 1
, чтобы указать на следующий байт в буфере для последующих вызовов ReadByte
или других методов чтения.
func (b *Buffer) ReadRune() (r rune, size int, err error) {
// Проверяем, пуст ли буфер
if b.empty() {
b.Reset()
return 0, 0, io.EOF
}
// Читаем следующий байт из буфера
c := b.buf[b.off]
// Если байт c меньше utf8.RuneSelf, это ASCII символ
if c < utf8.RuneSelf {
b.off++
b.lastRead = opReadRune1
return rune(c), 1, nil
}
// Декодируем UTF-8 кодовую точку
r, n := utf8.DecodeRune(b.buf[b.off:])
b.off += n
b.lastRead = readOp(n)
return r, n, nil
}
Аналогично ReadByte.
func (b *Buffer) UnreadRune() error {
// Проверяем, была ли последняя операция чтения успешной
if b.lastRead <= opInvalid {
return errors.New("bytes.Buffer: UnreadRune: previous operation was not a successful ReadRune")
}
// Проверяем, что текущее смещение b.off не меньше, чем количество байт,
// использованных для чтения последней кодовой точки (b.lastRead).
if b.off >= int(b.lastRead) {
// Возвращаемся на количество байт, равное b.lastRead, назад от текущего смещения b.off.
b.off -= int(b.lastRead)
}
// Сбрасываем lastRead, т.к. мы успешно вернули назад кодовую точку.
b.lastRead = opInvalid
return nil
}
Метод UnreadRune
позволяет вернуть назад только последнюю успешно прочитанную кодовую точку (rune
). В отличие от метода UnreadByte
, который может вернуть назад последний байт из любой операции чтения, UnreadRune
требует, чтобы последняя операция чтения была успешным вызовом ReadRune
. Этот метод полезен, когда требуется откатить операцию чтения кодовой точки в случае ошибки обработки данных или изменения логики алгоритма.
О чем мы и говорили выше, аналогичная логика.
func (b *Buffer) UnreadByte() error {
if b.lastRead == opInvalid {
return errUnreadByte
}
b.lastRead = opInvalid
if b.off > 0 {
b.off--
}
return nil
}
func (b *Buffer) ReadBytes(delim byte) (line []byte, err error) {
// Вызываем внутренний метод readSlice для чтения данных до разделителя.
slice, err := b.readSlice(delim)
// Возвращаем копию slice, так как внутренний буфер может быть изменен
// при последующих операциях.
line = append(line, slice...)
return line, err
}
func (b *Buffer) readSlice(delim byte) (line []byte, err error) {
i := IndexByte(b.buf[b.off:], delim)
end := b.off + i + 1
if i < 0 {
end = len(b.buf)
err = io.EOF
}
line = b.buf[b.off:end]
b.off = end
b.lastRead = opRead
return line, err
}
Метод ReadBytes
полезен для чтения данных из буфера до определенного разделителя, например, новой строки или конкретного символа. Он возвращает копию данных в line
, что предотвращает влияние последующих операций на внутренний буфер.
Пример:
func TryReadBytes() {
data := []byte("Hello, Gopher! How are you?")
buffer := bytes.NewBuffer(data)
// Читаем данные до первого пробела
line, err := buffer.ReadBytes(' ')
if err != nil {
fmt.Println("Error reading buffer:", err)
return
}
// Выводим прочитанные данные и удаленный разделитель
fmt.Printf("Read bytes until space: %s\n", line)
// Read bytes until space: Hello,
// Читаем оставшиеся данные в буфере
remaining := buffer.Bytes()
fmt.Printf("Remaining buffer data: %s\n", remaining)
// Remaining buffer data: Gopher! How are you?
}
func (b *Buffer) ReadString(delim byte) (line string, err error) {
slice, err := b.readSlice(delim)
return string(slice), err
}
Практически та же логика, что и у ReadBytes.
func NewBuffer(buf []byte) *Buffer { return &Buffer{buf: buf} }
Возращает экземпляр буфера с заданным буфером в виде массива байт.
func NewBufferString(s string) *Buffer {
return &Buffer{buf: []byte(s)}
}
В большинстве случаев это не нужно, так как достаточно просто создать буфер.
Надеюсь эта статья помогла тебе больше узнать о внутреннем строении Go, может даже просто освежил память, но спасибо, что ты дочитал до этого момента. bytes.Buffer
достаточно сильный пакет, в котором ты можешь найти оптимизированные решения для работы с массивами байт.