DEV Community

Cover image for Mastering the Bufio Package in Golang
Hilary Omondi
Hilary Omondi

Posted on • Edited on

Mastering the Bufio Package in Golang

Introduction

Bufio is a package of the standard library in golang that provides buffered I/O. It implements a buffered reader and writer that both implements the io.Reader and io.Writer interfaces.

What is Buffered I/O?

Buffered I/O is a technique that allows a program to read or write data in chunks rather than one byte at a time. This is useful because it allows the program to read or write data more efficiently. It also allows the program to read or write data more predictably.

In Go, this is done by using the bufio package. This package provides buffered readers and writers.

How to Use Bufio

The file we will be using in this guide is file.txt

file.txt
Enter fullscreen mode Exit fullscreen mode

Creating a Buffered Reader

To create a buffered reader, you can use the bufio.NewReader function. This function takes an io.Reader as an argument. This means that you can pass in any type that implements the io.Reader interface. This includes os.File, strings.Reader, and bytes.Buffer.

package main

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

func main() {
    file, err := os.Open("file.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    reader := bufio.NewReader(file)
    fmt.Println(reader)
}
Enter fullscreen mode Exit fullscreen mode

In the above example, we are creating a buffered reader from a file. We are then printing the buffered reader to the console. The buffered reader is a pointer to a bufio.Reader struct.

Reading from a Buffered Reader

To read from a buffered reader, you can use the bufio.Reader.Read function. This function takes a byte slice as an argument. This byte slice is where the data will be read into.

package main

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

func main() {
    file, err := os.Open("file.txt")
    if err != nil {
        fmt.Println(err)
        return
    }

    reader := bufio.NewReader(file)
    data := make([]byte, 100)
    _, err = reader.Read(data)
    if err != nil {
        fmt.Println(err)
        return
    }

    fmt.Println(string(data))
}
Enter fullscreen mode Exit fullscreen mode

In the above example, we are creating a buffered reader from a file. We are then reading 100 bytes from the file into a byte slice. We are then converting the byte slice to a string and printing it to the console.

Creating a Buffered Writer

To create a buffered writer, you can use the bufio.NewWriter function. This function takes an io.Writer as an argument. This means that you can pass in any type that implements the io.Writer interface. This includes os.File, strings.Builder, and bytes.Buffer.

package main

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

func main() {
    file, err := os.Create("file.txt")
    if err != nil{
        fmt.Println(err)
        return
    }
    defer file.Close()

    writer := bufio.NewWriter(file)
    fmt.Println(writer)
}
Enter fullscreen mode Exit fullscreen mode

In the above example, we are creating a buffered writer from a file. We are then printing the buffered writer to the console. The buffered writer is a pointer to a bufio.Writer struct.

Writing to a Buffered Writer

To write to a buffered writer, you can use the bufio.Writer.Write function. This function takes a byte slice as an argument. This byte slice is the data that will be written to the writer.

bufio.Writer.Flush is used to write the data to the writer. This function must be called before the program exits or the data will not be written to the writer.

package main

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

func main() {
    file, err := os.Create("file2.txt")
    if err != nil {
        fmt.Println(err)
        return
    }

    writer := bufio.NewWriter(file)
    _, err = writer.Write([]byte("Hello World!"))
    if err != nil {
        fmt.Println(err)
        return
    }

    err = writer.Flush()
    if err != nil {
        fmt.Println(err)
        return
    }
}

Enter fullscreen mode Exit fullscreen mode

This will create a file called file.txt and write Hello World! to it. The bufio.Writer.Write function will not write the data to the file until the bufio.Writer.Flush function is called.

Change the Buffer Size

The default buffer size for a buffered writer is 4096 bytes. This means that the data will be written to the writer in chunks of 4096 bytes. If you want to change the buffer size, you can use the bufio.NewWriterSize function.

package main

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

func main() {
    file, err := os.Create("file2.txt")
    if err != nil {
        fmt.Println(err)
        return
    }

    writer := bufio.NewWriterSize(file, 100)
    _, err = writer.Write([]byte("Hello World!"))
    if err != nil {
        fmt.Println(err)
        return
    }

    err = writer.Flush()
    if err != nil {
        fmt.Println(err)
        return
    }
}

Enter fullscreen mode Exit fullscreen mode

In the above program, we changed the buffer size to 100 bytes. This means that the data will be written to the writer in chunks of 100 bytes. This will make the writer slower but it will also use less memory.

Bufio vs. I/O

The main difference between buffered I/O and normal I/O is that buffered I/O reads or writes data in chunks rather than one byte at a time. While on the other side normal I/O reads or writes data one byte at a time. This might not seem like a big difference but it can make a big difference in performance.

In a case where you are reading or writing a lot of data, buffered I/O can be much faster than normal I/O. To see this, we can compare the performance of buffered I/O and normal I/O using benchmarks.

package main

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

func funcToWithIO() {
    file, err := os.Open("file.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    data := make([]byte, 100)
    for {
        _, err := file.Read(data)
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println(err)
            return
        }
    }
}

func funcToWithBufio() {
    file, err := os.Open("file.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    reader := bufio.NewReader(file)
    data := make([]byte, 100)
    for {
        _, err := reader.Read(data)
        if err == io.EOF {
            break
        }
        if err != nil {
            fmt.Println(err)
            return
        }
    }
}

func createFile() {
    file, err := os.Create("file.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    for i := 0; i < 1000000; i++ {
        file.Write([]byte("Hello World!"))
    }
}

func main() {
    createFile()
    funcToWithIO()
    funcToWithBufio()
}

Enter fullscreen mode Exit fullscreen mode

In the above example, we are creating a file called file.txt and writing Hello World! 1,000,000 times to it. We are then reading the file using normal I/O and buffered I/O. We are then benchmarking the two functions to see which one is faster

Other Bufio Functions

There are many other functions in the bufio package that can be used to read and write data. Here are some of the most commonly used functions:

bufio.Reader.ReadString

This function reads data until a specific delimiter is found. It returns a string containing the data up to and including the delimiter.

package main

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

func main() {
    file, err := os.Open("file.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    reader := bufio.NewReader(file)
    data, err := reader.ReadString('\n')
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(data)
}
Enter fullscreen mode Exit fullscreen mode

In this case, the bufio.Reader.ReadString function will read data from the file until it finds a new line character. It will then return the data up to and including the newline character.

bufio.Reader.ReadBytes

This function is similar to the bufio.Reader.ReadString function. The only difference is that it returns a byte slice instead of a string.

package main

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

func main() {
    file, err := os.Open("file.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    reader := bufio.NewReader(file)
    data, err := reader.ReadBytes('\n')
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(data)
}

Enter fullscreen mode Exit fullscreen mode

bufio.Reader.ReadSlice

This function is similar to the bufio.Reader.ReadString function. The only difference is that it returns a byte slice instead of a string. The difference between bufio.Reader.ReadSlice and bufio.Reader.ReadBytes is that bufio.Reader.ReadSlice will return an error if the delimiter is not found.

bufio.Reader.ReadLine

package main

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

func main() {
    file, err := os.Open("file.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    reader := bufio.NewReader(file)
    data, isPrefix, err := reader.ReadLine()
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(data, isPrefix)
}

Enter fullscreen mode Exit fullscreen mode

bufio.Reader.ReadRune

The bufio.Reader.ReadRune function reads a single UTF-8 encoded Unicode character and returns the Unicode code point. It returns an error if the character is not a valid Unicode character.

package main

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

func main() {
    file, err := os.Open("file.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    reader := bufio.NewReader(file)
    data, _, err := reader.ReadRune()
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(data)
}

Enter fullscreen mode Exit fullscreen mode

bufio.Reader.Read

The bufio.Reader.Read function reads data into a byte slice. It returns the number of bytes read and an error if any.

package main

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

func main() {
    file, err := os.Open("file.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    reader := bufio.NewReader(file)
    data := make([]byte, 5)
    n, err := reader.Read(data)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(n, data)
}

Enter fullscreen mode Exit fullscreen mode

bufio.Reader.Buffered

The bufio.Reader.Buffered function returns the number of bytes that can be read from the current buffer without blocking.

package main

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

func main() {
    file, err := os.Open("file.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    defer file.Close()

    reader := bufio.NewReader(file)
    fmt.Println(reader.Buffered())
}

Enter fullscreen mode Exit fullscreen mode

Other Bufio Types

bufio.Writer

The bufio.Writer type implements a buffered writer. It wraps an io.Writer and provides buffering and some help for textual I/O. It is not safe for concurrent use by multiple goroutines.

It has the following methods:

  1. bufio.NewWriter - creates a new buffered writer

  2. bufio.Writer.Flush - flushes any buffered data to the underlying writer

  3. bufio.Writer.Available - returns the number of bytes that can be written to the buffer without blocking

  4. bufio.Writer.Buffered - returns the number of bytes that are currently buffered

  5. bufio.Writer.WriteString - writes a string to the buffer

  6. bufio.Writer.WriteByte - writes a single byte to the buffer

  7. *bufio.Writer.WriteRune *- writes a single UTF-8 encoded Unicode character to the buffer

bufio.Scanner

The bufio.Scanner type implements a simple scanner for reading data. It wraps an io.Reader and provides a simple interface for reading data, line by line. It is not safe for concurrent use by multiple goroutines.

It has the following methods:

1.*bufio.NewScanner *- creates a new scanner

2.bufio.Scanner.Scan - advances the scanner to the next token, which will then be available through the Text method. It returns false when the scan stops, either by reaching the end of the input or by an error

3.bufio.Scanner.Text - returns the most recent token generated by a call to Scan as a newly allocated string holding its bytes.

4.bufio.Scanner.Bytes - returns the most recent token generated by a call to Scan as a slice of bytes that will be overwritten by the next call to Scan.

  1. bufio.Scanner.Err - returns the first non-nil error that was encountered by the Scanner.

Top comments (0)