Go by Example

Reading Files

os.ReadFile for small files, os.Open with bufio.Scanner for line-by-line reading, the io.Reader interface, and reading from stdin.

The os and bufio packages provide file reading at different granularities. os.ReadFile reads a whole file at once; bufio.Scanner reads line by line without loading the full file into memory.

os.ReadFile reads an entire file into a []byte in one call. It opens, reads, and closes the file for you. Suitable for config files, small assets, and test fixtures - anything with a known bounded size.

package main
 
import (
    "fmt"
    "os"
)
 
func main() {
    data, err := os.ReadFile("go.mod")
    if err != nil {
        panic(err)
    }
    fmt.Printf("read %d bytes\n", len(data))
    fmt.Println(string(data[:64])) // print first 64 bytes
}

For large files or line-oriented processing, open the file with os.Open and wrap it in a bufio.Scanner. The scanner reads one line at a time, keeping memory usage constant regardless of file size.

package main
 
import (
    "bufio"
    "fmt"
    "os"
)
 
func main() {
    f, err := os.Open("go.mod")
    if err != nil {
        panic(err)
    }
    defer f.Close()
 
    scanner := bufio.NewScanner(f)
    lineNum := 0
    for scanner.Scan() {
        lineNum++
        fmt.Printf("%3d: %s\n", lineNum, scanner.Text())
    }
    if err := scanner.Err(); err != nil {
        panic(err)
    }
}

os.Stdin implements io.Reader, so the same bufio.Scanner pattern reads from standard input. This is the basis of Unix-style filter programs that accept piped input.

package main
 
import (
    "bufio"
    "fmt"
    "os"
    "strings"
)
 
func main() {
    scanner := bufio.NewScanner(os.Stdin)
    for scanner.Scan() {
        line := scanner.Text()
        fmt.Println(strings.ToUpper(line))
    }
    if err := scanner.Err(); err != nil {
        fmt.Fprintln(os.Stderr, "read error:", err)
        os.Exit(1)
    }
}

In production

os.ReadFile loads the entire file into memory - fine for configs and test fixtures, unsafe for unbounded user uploads or large log files. For large or streaming reads, open with os.Open and wrap with bufio.Reader or bufio.Scanner. Always call defer f.Close() immediately after a successful os.Open - not after the error check, not at the end of the function, but on the very next line so it is never accidentally omitted. bufio.Scanner has a default line buffer of 64 KB; if you expect lines longer than that (e.g. minified JSON), use scanner.Buffer(make([]byte, maxSize), maxSize) before the first Scan(). Check scanner.Err() after the loop - a scanner that runs out of input and a scanner that encounters a read error both exit the loop, but only one is an error.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free