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.Openopens the file.bufio.Scannerorbufio.Readerhelps 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.Filegives you access to the roll.bufio.Scanneracts 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.
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 whenmainfinishes.bufio.NewScanner(file)creates a scanner that reads from the file.scanner.Scan()moves to the next line and returnstruewhile 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
Scannerprovides
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
-
os.Open("example.txt")- Go opens the file.
filenow refers to that file.
-
defer file.Close()- Go schedules the file to be closed later.
- This helps prevent resource leaks.
-
scanner := bufio.NewScanner(file)- A scanner is created to read from the file.
- By default, it splits input by lines.
-
First
scanner.Scan()- Reads the first line:
apple - Returns
true
- Reads the first line:
-
scanner.Text()- Returns
"apple" fmt.Printlnprints it.
- Returns
-
Second
scanner.Scan()- Reads
banana - Returns
true
- Reads
-
Third
scanner.Scan()- Reads
cherry - Returns
true
- Reads
-
Next
scanner.Scan()- No more lines remain
- Returns
false
-
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.
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,
filemay 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
| Feature | bufio.Scanner | bufio.Reader |
|---|---|---|
| Best for | Simple line-by-line reading | More control over reading |
| Ease of use | Very easy | Slightly more manual |
| Returns newline | No | Can keep newline |
| Large lines | Limited by token buffer unless adjusted | Better for very large lines |
| Custom delimiters | Limited but possible with split functions | Flexible |
os.ReadFile vs line-by-line reading
| Approach | When to use | Trade-off |
|---|---|---|
os.ReadFile | Small files you want all at once | Simpler, but loads everything into memory |
Scanner/Reader | Large files or streaming text | More efficient, slightly more code |
Which should beginners choose?
- Use
bufio.Scannerfor most text files. - Use
bufio.Readerwhen you need more control or must handle very long lines. - Use
os.ReadFileonly when reading the entire file at once makes sense.
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
}
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
Keep learning
Related questions
Are Go Maps Passed by Value or Reference?
Learn Go map parameter and return semantics: maps are passed by value, while copies share underlying data. Know when map pointers are useful.
Authentication for Go REST APIs: Sessions, Tokens, and Middleware
Learn practical authentication patterns for Go REST APIs, including password hashing, server sessions, bearer tokens, middleware, and security rules.
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.