Question

How to Read a File Line by Line in Go

gostringfileparsingline

Question

I cannot find a file.ReadLine function in Go.

How can I read a file line by line in Go?

Short Answer

By the end of this page, you will understand how to read a file line by line in Go, why bufio.Scanner is usually the simplest choice, when to use bufio.Reader instead, and how this pattern is used in real programs for logs, configuration files, and text processing.

Concept

In Go, reading a file line by line is usually done with the bufio package rather than a method directly on os.File.

An os.File gives you access to the file itself, but buffered helpers such as bufio.Scanner and bufio.Reader make reading text much easier.

The most common beginner-friendly approach is:

scanner := bufio.NewScanner(file)
for scanner.Scan() {
    line := scanner.Text()
    // use line
}
if err := scanner.Err(); err != nil {
    // handle error
}

Why this matters:

  • Many real programs process text files one line at a time.
  • Reading line by line is memory-efficient for large files.
  • It lets you validate, transform, or filter data as you read it.
  • This pattern is common for logs, CSV-like text, config files, and command-line tools.

In short, Go separates responsibilities:

  • os.Open opens the file.
  • bufio.Scanner or bufio.Reader helps you read structured text from it.
  • Your code decides what to do with each line.

Mental Model

Think of a file as a long roll of paper.

  • os.File gives you access to the roll.
  • bufio.Scanner acts like a reading guide that moves from one line break to the next.
  • Each time you call Scan(), it points to the next line.
  • Text() gives you the current line.

So instead of asking the file itself, “give me one line,” you wrap it in a tool designed specifically for line-by-line reading.

Take Quiz

Syntax and Examples

Using bufio.Scanner

This is the most common way to read a text file line by line in Go.

package main

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

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

    scanner := bufio.NewScanner(file)

    for scanner.Scan() {
        line := scanner.Text()
        fmt.Println(line)
    }

    if err := scanner.Err(); err != nil {
        fmt.Println("error reading file:", err)
    }
}

How it works

  • os.Open("example.txt") opens the file for reading.
  • defer file.Close() ensures the file is closed when main finishes.
  • bufio.NewScanner(file) creates a scanner that reads from the file.
  • scanner.Scan() moves to the next line and returns true while there is data.
  • scanner.Text() returns the current line as a string.
  • scanner.Err() reports any error that happened during scanning.

Alternative: using bufio.Reader

If you need more control, you can use bufio.Reader.

package main

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

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

    reader := bufio.NewReader(file)

    for {
        line, err := reader.ReadString('\n')
        if err == io.EOF {
            if line != "" {
                fmt.Print(line)
            }
            break
        }
        if err != nil {
            fmt.Println("error reading file:", err)
            return
        }

        fmt.Print(line)
    }
}

Use bufio.Reader when:

  • you want to keep the newline character
  • you need custom delimiter handling
  • you need more control than Scanner provides

Step by Step Execution

Consider this code:

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

scanner := bufio.NewScanner(file)
for scanner.Scan() {
    fmt.Println(scanner.Text())
}

if err := scanner.Err(); err != nil {
    fmt.Println(err)
}

Assume example.txt contains:

apple
banana
cherry

Step by step

  1. os.Open("example.txt")

    • Go opens the file.
    • file now refers to that file.
  2. defer file.Close()

    • Go schedules the file to be closed later.
    • This helps prevent resource leaks.
  3. scanner := bufio.NewScanner(file)

    • A scanner is created to read from the file.
    • By default, it splits input by lines.
  4. First scanner.Scan()

    • Reads the first line: apple
    • Returns true
  5. scanner.Text()

    • Returns "apple"
    • fmt.Println prints it.
  6. Second scanner.Scan()

    • Reads banana
    • Returns true
  7. Third scanner.Scan()

    • Reads cherry
    • Returns true
  8. Next scanner.Scan()

    • No more lines remain
    • Returns false
  9. scanner.Err()

    • Checks whether scanning stopped because of an error
    • If there was no error, it returns nil

This pattern is simple because the loop naturally stops at the end of the file.

Real World Use Cases

Reading files line by line is common in many Go programs.

Log processing

A script can scan a server log and count error lines:

if strings.Contains(line, "ERROR") {
    errorCount++
}

Configuration loading

Some tools read simple config files line by line, skipping blank lines and comments.

if line == "" || strings.HasPrefix(line, "#") {
    continue
}

Importing text data

A program can read user IDs, email addresses, or product codes from a file and process them one at a time.

Command-line utilities

Many CLI tools stream input files instead of loading everything into memory at once.

Data cleanup scripts

A Go script can read each line, trim spaces, validate the format, and write cleaned output to another file.

Take Quiz

Real Codebase Usage

In real projects, developers usually wrap line-reading logic in reusable functions and combine it with validation and error handling.

Common pattern: open, scan, process

func processFile(path string) error {
    file, err := os.Open(path)
    if err != nil {
        return err
    }
    defer file.Close()

    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := strings.TrimSpace(scanner.Text())
        if line == "" {
            continue
        }

        // process line
    }

    return scanner.Err()
}

Guard clauses

Developers often return early when a file cannot be opened or a line is invalid.

if err != nil {
    return err
}

Validation while reading

Instead of reading the whole file first, code often validates each line immediately.

if !strings.Contains(line, ":") {
    continue
}

Streaming large files

For large logs or exports, line-by-line reading avoids loading the entire file into memory.

Error reporting with line numbers

A common pattern is to track line numbers for better debugging.

lineNumber := 0
for scanner.Scan() {
    lineNumber++
    line := scanner.Text()
    _ = line
}

Common Mistakes

1. Forgetting to check scanner.Err()

Broken example:

scanner := bufio.NewScanner(file)
for scanner.Scan() {
    fmt.Println(scanner.Text())
}

Problem:

  • The loop stops on both end-of-file and read error.
  • Without scanner.Err(), you cannot tell whether something went wrong.

Fix:

if err := scanner.Err(); err != nil {
    fmt.Println("read error:", err)
}

2. Forgetting to close the file

Broken example:

file, _ := os.Open("example.txt")
scanner := bufio.NewScanner(file)

Problem:

  • Open files use system resources.

Fix:

defer file.Close()

3. Ignoring the error from os.Open

Broken example:

file, _ := os.Open("missing.txt")
scanner := bufio.NewScanner(file)

Problem:

  • If opening fails, file may be invalid and your program logic becomes unsafe.

Fix:

file, err := os.Open("missing.txt")
if err != nil {
    return
}

4. Using Scanner for very long lines without knowing its limit

bufio.Scanner has a default token size limit. Very long lines can cause scanning to fail.

Possible fix:

scanner := bufio.NewScanner(file)
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024)

If you need to handle very large lines, bufio.Reader may be a better choice.

5. Expecting newline characters from scanner.Text()

scanner.Text() returns the line content without the line ending.

If you need the newline, use bufio.Reader with ReadString('\n') instead.

Comparisons

bufio.Scanner vs bufio.Reader

Featurebufio.Scannerbufio.Reader
Best forSimple line-by-line readingMore control over reading
Ease of useVery easySlightly more manual
Returns newlineNoCan keep newline
Large linesLimited by token buffer unless adjustedBetter for very large lines
Custom delimitersLimited but possible with split functionsFlexible

os.ReadFile vs line-by-line reading

ApproachWhen to useTrade-off
os.ReadFileSmall files you want all at onceSimpler, but loads everything into memory
Scanner/ReaderLarge files or streaming textMore efficient, slightly more code

Which should beginners choose?

  • Use bufio.Scanner for most text files.
  • Use bufio.Reader when you need more control or must handle very long lines.
  • Use os.ReadFile only when reading the entire file at once makes sense.
Take Quiz

Cheat Sheet

Quick pattern with bufio.Scanner

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

scanner := bufio.NewScanner(file)
for scanner.Scan() {
    line := scanner.Text()
    _ = line
}
if err := scanner.Err(); err != nil {
    return
}

Rules to remember

  • os.Open() opens the file.
  • defer file.Close() closes it later.
  • bufio.NewScanner(file) creates a line scanner.
  • scanner.Scan() moves to the next line.
  • scanner.Text() gets the current line.
  • scanner.Err() checks for errors after the loop.

Use Scanner when

  • you want simple line-by-line reading
  • lines are normal text size
  • you do not need newline characters

Use Reader when

  • you need to keep \n
  • you need custom delimiters
  • lines may be very large

Common edge case

Very long lines can exceed Scanner's default buffer.

scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)

FAQ

Is there a ReadLine method on os.File in Go?

No. In Go, line-based reading is usually done with helpers from the bufio package, such as bufio.Scanner or bufio.Reader.

What is the easiest way to read a file line by line in Go?

Use bufio.Scanner. It is simple, readable, and designed for token-based input such as lines.

Does scanner.Text() include the newline character?

No. It returns the line content without the trailing newline.

What should I use for very large lines?

If lines may be very large, either increase the scanner buffer or use bufio.Reader for more control.

Should I use os.ReadFile instead?

Only if the file is small and you want all its contents at once. For large files, line-by-line reading is more memory-efficient.

Why do I need to check scanner.Err() after the loop?

Because Scan() returns false for both end-of-file and actual read errors. scanner.Err() tells you whether an error occurred.

Can I skip empty lines while reading?

Yes. A common pattern is:

if strings.TrimSpace(line) == "" {
    continue
}
Take Quiz

Mini Project

Description

Build a small Go program that reads a log file line by line and counts how many lines contain the word ERROR. This demonstrates opening a file, scanning line by line, checking each line, and handling errors properly.

Goal

Create a command-line program that prints the total number of error lines in a text file.

Requirements

  • Open a text file named app.log
  • Read the file one line at a time
  • Count lines that contain the word ERROR
  • Print the final count
  • Handle file and scanning errors correctly
Take Quiz

Keep learning

Related questions

View more