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:
- ReadLines: Read all lines from a file into a slice
- CountLines: Count the number of lines in a file
- ReadFirstN: Read the first N lines from a file
- GrepFile: Find all lines containing a substring
- 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
- All functions must handle file not found errors gracefully
- Use buffered reading for efficiency
- Properly close files after reading
- Handle both Unix and Windows line endings
- 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:
- Open file with
os.Open() - Defer file closure with
defer file.Close() - Use
bufio.Scannerfor efficient line-by-line reading - Check for scanner errors after loop
- Return wrapped errors with context
This pattern ensures proper resource cleanup and clear error messages.
Bonus Challenges
- Tail: Implement Unix tail command
- ReadChunks: Read file in chunks of N bytes
- FileSearch: Search for files matching a pattern in a directory
- 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.