File Reader

Exercise: File Reader

Difficulty - Beginner

Learning Objectives

  • Master file I/O operations in Go
  • Practice reading files line by line
  • Learn error handling patterns
  • Understand buffered vs unbuffered reading

Problem Statement

Create a FileUtils package with common file reading operations:

  1. ReadLines: Read all lines from a file into a slice
  2. CountLines: Count the number of lines in a file
  3. ReadFirstN: Read the first N lines from a file
  4. GrepFile: Find all lines containing a substring
  5. FileStats: Get file statistics

Function Signatures

 1package fileutils
 2
 3// ReadLines reads all lines from a file
 4func ReadLines(filename string)
 5
 6// CountLines returns the number of lines in a file
 7func CountLines(filename string)
 8
 9// ReadFirstN reads the first n lines from a file
10func ReadFirstN(filename string, n int)
11
12// GrepFile returns all lines containing the search string
13func GrepFile(filename, search string)
14
15// FileStats contains file statistics
16type FileStats struct {
17    Lines int
18    Words int
19    Bytes int64
20}
21
22// GetFileStats returns statistics about a file
23func GetFileStats(filename string)

Example Usage

 1package main
 2
 3import (
 4    "fmt"
 5    "log"
 6    "fileutils"
 7)
 8
 9func main() {
10    filename := "example.txt"
11
12    // ReadLines
13    lines, err := fileutils.ReadLines(filename)
14    if err != nil {
15        log.Fatal(err)
16    }
17    fmt.Printf("Total lines: %d\n", len(lines))
18
19    // CountLines
20    count, err := fileutils.CountLines(filename)
21    if err != nil {
22        log.Fatal(err)
23    }
24    fmt.Printf("Line count: %d\n", count)
25
26    // ReadFirstN
27    first5, err := fileutils.ReadFirstN(filename, 5)
28    if err != nil {
29        log.Fatal(err)
30    }
31    fmt.Println("First 5 lines:", first5)
32
33    // GrepFile
34    matches, err := fileutils.GrepFile(filename, "error")
35    if err != nil {
36        log.Fatal(err)
37    }
38    fmt.Printf("Lines with 'error': %d\n", len(matches))
39
40    // GetFileStats
41    stats, err := fileutils.GetFileStats(filename)
42    if err != nil {
43        log.Fatal(err)
44    }
45    fmt.Printf("Stats: %d lines, %d words, %d bytes\n",
46        stats.Lines, stats.Words, stats.Bytes)
47}

Requirements

  1. All functions must handle file not found errors gracefully
  2. Use buffered reading for efficiency
  3. Properly close files after reading
  4. Handle both Unix and Windows line endings
  5. GetFileStats should count words using whitespace as delimiter

Solution

Click to see the complete solution
  1package fileutils
  2
  3import (
  4    "bufio"
  5    "fmt"
  6    "os"
  7    "strings"
  8)
  9
 10// ReadLines reads all lines from a file
 11func ReadLines(filename string) {
 12    file, err := os.Open(filename)
 13    if err != nil {
 14        return nil, fmt.Errorf("opening file: %w", err)
 15    }
 16    defer file.Close()
 17
 18    var lines []string
 19    scanner := bufio.NewScanner(file)
 20
 21    for scanner.Scan() {
 22        lines = append(lines, scanner.Text())
 23    }
 24
 25    if err := scanner.Err(); err != nil {
 26        return nil, fmt.Errorf("reading file: %w", err)
 27    }
 28
 29    return lines, nil
 30}
 31
 32// CountLines returns the number of lines in a file
 33func CountLines(filename string) {
 34    file, err := os.Open(filename)
 35    if err != nil {
 36        return 0, fmt.Errorf("opening file: %w", err)
 37    }
 38    defer file.Close()
 39
 40    count := 0
 41    scanner := bufio.NewScanner(file)
 42
 43    for scanner.Scan() {
 44        count++
 45    }
 46
 47    if err := scanner.Err(); err != nil {
 48        return 0, fmt.Errorf("reading file: %w", err)
 49    }
 50
 51    return count, nil
 52}
 53
 54// ReadFirstN reads the first n lines from a file
 55func ReadFirstN(filename string, n int) {
 56    file, err := os.Open(filename)
 57    if err != nil {
 58        return nil, fmt.Errorf("opening file: %w", err)
 59    }
 60    defer file.Close()
 61
 62    lines := make([]string, 0, n)
 63    scanner := bufio.NewScanner(file)
 64
 65    for scanner.Scan() && len(lines) < n {
 66        lines = append(lines, scanner.Text())
 67    }
 68
 69    if err := scanner.Err(); err != nil {
 70        return nil, fmt.Errorf("reading file: %w", err)
 71    }
 72
 73    return lines, nil
 74}
 75
 76// GrepFile returns all lines containing the search string
 77func GrepFile(filename, search string) {
 78    file, err := os.Open(filename)
 79    if err != nil {
 80        return nil, fmt.Errorf("opening file: %w", err)
 81    }
 82    defer file.Close()
 83
 84    var matches []string
 85    scanner := bufio.NewScanner(file)
 86
 87    for scanner.Scan() {
 88        line := scanner.Text()
 89        if strings.Contains(line, search) {
 90            matches = append(matches, line)
 91        }
 92    }
 93
 94    if err := scanner.Err(); err != nil {
 95        return nil, fmt.Errorf("reading file: %w", err)
 96    }
 97
 98    return matches, nil
 99}
100
101// FileStats contains file statistics
102type FileStats struct {
103    Lines int
104    Words int
105    Bytes int64
106}
107
108// GetFileStats returns statistics about a file
109func GetFileStats(filename string) {
110    file, err := os.Open(filename)
111    if err != nil {
112        return FileStats{}, fmt.Errorf("opening file: %w", err)
113    }
114    defer file.Close()
115
116    // Get file size
117    info, err := file.Stat()
118    if err != nil {
119        return FileStats{}, fmt.Errorf("stating file: %w", err)
120    }
121
122    stats := FileStats{
123        Bytes: info.Size(),
124    }
125
126    scanner := bufio.NewScanner(file)
127    for scanner.Scan() {
128        stats.Lines++
129        line := scanner.Text()
130        stats.Words += len(strings.Fields(line))
131    }
132
133    if err := scanner.Err(); err != nil {
134        return FileStats{}, fmt.Errorf("reading file: %w", err)
135    }
136
137    return stats, nil
138}

Explanation

All functions follow a common pattern:

  1. Open file with os.Open()
  2. Defer file closure with defer file.Close()
  3. Use bufio.Scanner for efficient line-by-line reading
  4. Check for scanner errors after loop
  5. Return wrapped errors with context

This pattern ensures proper resource cleanup and clear error messages.

Bonus Challenges

  1. Tail: Implement Unix tail command
  2. ReadChunks: Read file in chunks of N bytes
  3. FileSearch: Search for files matching a pattern in a directory
  4. WordFrequency: Count word frequencies in a file

Key Takeaways

  • Always close files using defer file.Close()
  • Use bufio.Scanner for line-by-line reading
  • Check scanner.Err() after the loop completes
  • Wrap errors with context using fmt.Errorf()
  • Handle both EOF and errors properly

File I/O is fundamental to many programs. Understanding efficient reading patterns and proper error handling is essential for robust Go applications.