golang

Golang: bytes.Buffer изнутри

  • среда, 10 июля 2024 г. в 00:00:06
https://habr.com/ru/articles/827550/

Работая с кодом на Go, любому специалисту приходилось сталкиваться со стандартным пакетом bytes . Внутри него лежит определение Buffer . Что же это такое?

Определение 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 that UnreadRune and UnreadByte 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 разобрались и какие она значения принимает.

Разберемся с методами буфера.

Bytes

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, так как это тот же самый участок памяти

AvailableBuffer

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
}

String

Тут все просто, он возвращает содержимое непрочитанной части буфера в виде строки. Если указатель на 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

Метод empty используется для определения того, пуст ли буфер, с точки зрения чтения данных. Возвращаемое значение true указывает на то, что непрочитанная часть буфера исчерпана (то есть все данные уже прочитаны или буфер не содержит данных вообще), а false означает, что в буфере еще есть данные, которые могут быть прочитаны.

func (b *Buffer) empty() bool { return len(b.buf) <= b.off }

Он используется в других методах, которые мы затронем в дальнейшем.

Len

Метод 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

Метод 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
}

Available

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 байта свободного пространства.

Truncate

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
}

Reset

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:
}

Grow

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
}

Write

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
}

Тут можно обойтись без примера, так как выше в статье использовался этот метод.

WriteString

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, но оптимизирован для работы со строками. Он предоставляет удобный способ добавления содержимого строки в буфер.

ReadFrom

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.
}

WriteTo

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
}

WriteByte

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
}

WriteRune

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
}

Ну для чего он по предыдущим примерам понятно, поэтому можно обойтись без примера.

Read

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
}

Предназначен для чтения данных из буфера в заданный срез.

Next

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 буфер сместился на соответствующее количество байт
	// и больше не содержит первых прочитанных данных.
}

ReadByte

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 или других методов чтения.

ReadRune

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.

UnreadRune

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. Этот метод полезен, когда требуется откатить операцию чтения кодовой точки в случае ошибки обработки данных или изменения логики алгоритма.

UnreadByte

О чем мы и говорили выше, аналогичная логика.

func (b *Buffer) UnreadByte() error {
	if b.lastRead == opInvalid {
		return errUnreadByte
	}
	b.lastRead = opInvalid
	if b.off > 0 {
		b.off--
	}
	return nil
}

ReadBytes

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?
}

ReadString

func (b *Buffer) ReadString(delim byte) (line string, err error) {
	slice, err := b.readSlice(delim)
	return string(slice), err
}

Практически та же логика, что и у ReadBytes.

NewBuffer

func NewBuffer(buf []byte) *Buffer { return &Buffer{buf: buf} }

Возращает экземпляр буфера с заданным буфером в виде массива байт.

NewBufferString

func NewBufferString(s string) *Buffer {
	return &Buffer{buf: []byte(s)}
}

В большинстве случаев это не нужно, так как достаточно просто создать буфер.

Заключение

Надеюсь эта статья помогла тебе больше узнать о внутреннем строении Go, может даже просто освежил память, но спасибо, что ты дочитал до этого момента. bytes.Buffer достаточно сильный пакет, в котором ты можешь найти оптимизированные решения для работы с массивами байт.