Go
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
46: File I/O with os and bufio
How do I just read a whole file into memory quickly?
If you're dealing with a small configuration file or a simple text document, don't overcomplicate things. You don't need a buffer or a complex loop. The os package gives us ReadFile, which does exactly what it sounds like: it opens the file, reads the entire thing into a byte slice, and closes the file for you in one shot.
package main
import (
"fmt"
"os"
)
func main() {
// Let's assume we have a 'config.txt' file
data, err := os.ReadFile("config.txt")
if err != nil {
fmt.Println("Error reading file:", err)
return
}
// data is a []byte, so we cast it to a string to print it
fmt.Println(string(data))
}
It's convenient, but be careful. If you try to use this on a 10GB server log, your program will crash because it tries to shove that entire 10GB into your RAM. That's where we need a different approach.
What's the best way to process a massive file line-by-line?
When the file is too big for memory, we use bufio.Scanner. Think of it like a sliding window that reads the file in small chunks. This is the bread and butter of log processing. You open the file with os.Open, wrap it in a scanner, and iterate through it.
package main
import (
"bufio"
"fmt"
"log"
"os"
"strings"
)
func main() {
file, err := os.Open("server.log")
if err != nil {
log.Fatal(err)
}
// I always put the Close() right after the error check.
// It ensures the file descriptor is released even if the function panics.
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
// Let's only print lines that contain "ERROR"
if strings.Contains(line, "ERROR") {
fmt.Println("Found an issue:", line)
}
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
}
One thing to watch out for: scanner.Scan() has a default buffer size. If you have a single line of text that is ridiculously long (like a giant JSON blob on one line), the scanner will fail. If that happens, you'll need to use reader.ReadLine() or increase the buffer manually, but for 95% of use cases, the scanner is your best bet.
How do I write to a file without overwriting everything?
Writing is where people usually get tripped up because os.WriteFile behaves like ReadFileβit handles everything at once, but it truncates (wipes) the file if it already exists. If you're building a logger, you want to append to the file.
To do this, you use os.OpenFile with specific flags. I usually pair this with bufio.Writer because writing to a disk is slow. Buffering collects your data in memory and writes it in larger chunks, which is significantly more performant.
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
// O_APPEND: Add to the end of the file
// O_CREATE: Create the file if it doesn't exist
// O_WRONLY: Open for writing only
file, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
fmt.Println(err)
return
}
defer file.Close()
writer := bufio.NewWriter(file)
_, err = writer.WriteString("User logged in: admin\n")
if err != nil {
fmt.Println(err)
}
// CRITICAL: bufio.Writer stores data in a buffer.
// If you don't call Flush(), your last few lines might never actually hit the disk.
writer.Flush()
}
That Flush() call is the most common bug I see in junior Go code. If your program finishes and your log file is empty, 9 times out of 10, you forgot to flush the buffer.
π Practical Task
Build a Log Filtering Tool
Create a program that reads a file named input.log and writes all lines containing the word "CRITICAL" into a new file named alerts.txt.
- The program should use
bufio.Scannerto read the input file to ensure it can handle files of any size. - The program should use
bufio.Writerto write the filtered lines to the output file. - Ensure that you properly close both files using
deferand that you callFlush()on your writer before the program exits. - Handle errors for opening and creating the files gracefully.
There are no comments for now.