Golang: bytes.Buffer from the inside

When working with Go code, any specialist has had to deal with the standard package bytes . Inside it lies the definition Buffer . What is it?

Definition of bytes.Buffer

By myself bytes.Buffer is a structure.

type Buffer struct {
	buf      []byte // содержимое - это байты buf[off : len(buf)]
	off      int    // читает по &buf[off], пишет по &buf[len(buf)]
	lastRead readOp // последняя операция чтения, чтобы Unread* могло работать корректно
}

Buffer is a variable-sized buffer that can be used to read and write data. When a buffer is initialized with a zero value, it will be empty and ready to be used.

Structure fields Buffer in Go are used to manage the internal state of a buffer when reading and writing data.

Let's look at each field in the structure:

  • buf – stores buffer data as a slice of bytes. All read and write operations are performed using this slice.

  • off – defines the current offset for reading data from the buffer. Data is read from the position buf[off]. Position off increases as data is read. Data is always written to the end of the buffer at address &buf[len(buf)]

  • lastRead – stores the type of the last read operation (eg. Read, UnreadByteetc.), which allows methods Unread* (For example, UnreadByte, UnreadRune) to perform its functions correctly. This field is used to control the operations of undoing the last read, in order to return the read data back to the buffer.

Okay, so we've figured out what fields are needed for what, but we need to look at the definition. readOp .

type readOp int8

We see that in essence it is int8 type. Next, let's look at the description of this type.

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 they are converted to int they correspond to the rune size that was read.

What is translated:

Constants readOp describe the last action performed on the buffer, so UnreadRune And UnreadByte can check for invalid usage. Constants opReadRuneX are chosen in such a way that when transformed into int they corresponded to the size of the rune that was read.

Let's look at the initialization readOp .

const (
	opRead      readOp = -1 // Любая другая операция на чтение
	opInvalid   readOp = 0  // Операция не на чтение
	opReadRune1 readOp = 1  // Читающая руна размером 1
	opReadRune2 readOp = 2  // Читающая руна размером 2
	opReadRune3 readOp = 3  // Читающая руна размером 3
	opReadRune4 readOp = 4  // Читающая руна размером 4
)

Good with readOp We figured out what values ​​it takes.

Let's look at the buffer methods.

Bytes

func (b *Buffer) Bytes() []byte { return b.buf[b.off:] }

Method Bytes in the structure Buffer returns a slice containing the unread data from the buffer. Let's take a closer look at what it does and what it's for.

The method is used to obtain a slice of bytes representing all data that has not yet been read from the buffer. This allows easy access to the remaining contents of the buffer without the need for additional data copying.

The method returns a slice b.buf[b.off:]which starts from the current offset b.off and goes to the end of the buffer. Thus, it returns all the data that has not yet been read.

Example of use:

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

Method Bytes is efficient because it returns a slice without any extra allocations or data copies. It simply provides access to the existing data, making it fast and memory efficient.

Important points:

  • Unread data: The method returns only unread data starting from the current offset b.off

  • Buffer changes: Any changes to the source buffer will be reflected in the slice returned by the method. Bytessince it is the same memory area

AvailableBuffer

func (b *Buffer) AvailableBuffer() []byte { return b.buf[len(b.buf):] }

Method AvailableBuffer in the structure Buffer returns an empty slice with a capacity equal to the available space in the buffer. This slice is intended for subsequent addition of data and passing to the method call Buffer.Write. The buffer is valid only until the next write operation in b.

AvailableBuffer is designed to provide access to free space in a buffer. It returns an empty slice that starts from the end of the current data in the buffer. This slice can be used to add new data before the next call. Buffer.Write.

Example:

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

It's simple, it returns the contents of the unread portion of the buffer as a string. If the pointer to Buffer is nil, then the method returns a string "<nil>". This method is useful for getting a string representation of the data in the buffer, and can be useful for debugging.

func (b *Buffer) String() string {
	if b == nil {
		// специальный случай, полезен в отладке
		return "<nil>"
	}
	return string(b.buf[b.off:])
}

Example:

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

Method empty used to determine if the buffer is empty from the point of view of reading data. Return Value true indicates that the unread portion of the buffer is exhausted (that is, all data has already been read or the buffer contains no data at all), and false means that there is still data in the buffer that can be read.

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

It is used in other methods that we will cover later.

Len

Method Len returns the number of bytes in the unread portion of the buffer.

func (b *Buffer) Len() int { return len(b.buf) - b.off }

Example of use:

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

Method Cap is used to obtain the capacity of the buffer, which is the total number of bytes that the dummy byte slice can hold. b.buf. The capacity determines how much data can be held in the buffer before additional memory needs to be allocated to increase the slice size.

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 used to determine the number of bytes that can still be added to the buffer without allocating additional memory.

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
}

Explanation:

  • Initializing the buffer: Buffer buffer is created with the data “Hello, world!”. At the beginning Available shows 0since the buffer is filled to its capacity (13 bytes).

  • Adding data: We write the string “Additional data.” to the buffer. The length of this string is 17 bytes. After writing, the buffer is not automatically expanded, and bytes available for writing remain.

  • Reacquiring available space: After recording Available shows 2because after writing the line “Additional data.” there are 2 bytes of free space left in the buffer.

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

Method Truncate designed to trim the buffer to a specified length nleaving only the first ones n unread bytes.

Example:

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 // устанавливаем последнюю операцию чтения в 
                           // неопределенное состояние про которое рассказывалось в начале статьи
}

Method Reset is used to flush the contents of a buffer, making it empty but still preserving the memory allocated to it for future writes. It is useful when you need to reuse a buffer while preserving the allocated memory for efficient resource management. It also allows you to quickly clear the contents of a buffer without having to reallocate memory. Example:

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] // обрезаем буфер до новой емкости
}

It is used to increase the buffer capacity so that enough space is guaranteed to write more n byte. It is useful when you know in advance how much data you are going to write to the buffer and want to avoid re-allocations of memory on each write operation. Also see the definition of the internal method 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
}

Let's also look at the definition 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
}

It is used for fast growth when the internal buffer only requires rewriting and returns the index to which the bytes should be written and whether this was successful.

Example:

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
}

Here we can do without an example, since this method was used above in the article.

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
}

Method WriteString similar to the method Writebut is optimized for working with strings. It provides a convenient way to append the contents of a string to a buffer.

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

The method is designed to read data from io.Reader to EOF and adding them to the buffer, increasing its size if necessary. It is also worth noting the definition of the size const MinRead = 512 , MinRead – this is the minimum fragment size passed to the call Read by using Buffer.ReadFrom. If there are bytes in the buffer MinReadexceeding what is required to store the contents r, ReadFrom will not increase the base buffer, which is quite useful.

ReadFrom efficient for reading data from io.Reader and adding them to the buffer without the need to create an intermediate buffer. It dynamically increases the buffer capacity to accommodate all the data.

Example:

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
}

This method is useful when you want to pass the contents of a buffer directly to another io.Writersuch as a file or a network connection.

Example of use:

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
}

Method WriteByte is useful for appending individual bytes to the end of a buffer. It automatically increases the buffer size when needed, allowing for efficient appending of data. Example usage:

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
}

Well, it is clear from the previous examples what it is for, so we can do without an example.

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
}

Designed to read data from the buffer into a specified slice.

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
}

Returns a slice containing the following n byte from the buffer, advancing the read pointer (b.off) as if these bytes were read using the method Buffer.Read. This method is particularly useful for efficiently reading sequential blocks of data from a buffer, such as when processing a data stream or parsing messages.

Example:

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
}

This method is suitable for situations where you need to process data one byte at a time from a buffer, such as when parsing structured data or reading text files. After a successful call ReadByteoffset b.off increases by 1to point to the next byte in the buffer for subsequent calls ReadByte or other reading methods.

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
}

Similar to 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
}

Method UnreadRune allows you to return back only the last successfully read code point (rune). Unlike the method UnreadBytewhich can return the last byte from any read operation, UnreadRune requires the last read operation to be a successful call ReadRune. This method is useful when you need to roll back a code point read operation in case of a data processing error or a change in the algorithm logic.

UnreadByte

What we talked about above, similar logic.

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
}

Method ReadBytes useful for reading data from a buffer up to a specific delimiter, such as a newline or a specific character. It returns a copy of the data in linewhich prevents subsequent operations from affecting the internal buffer.

Example:

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
}

Almost the same logic as ReadBytes.

NewBuffer

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

Returns a buffer instance with the given buffer as a byte array.

NewBufferString

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

In most cases this is not necessary, as simply creating a buffer is sufficient.

Conclusion

I hope this article helped you learn more about Go's internals, maybe even just refreshed your memory, but thanks for reading this far. bytes.Buffer quite a strong package, in which you can find optimized solutions for working with byte arrays.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *