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
120: io.Copy and io.ReadAll
When you're dealing with data streams in Go, you'll eventually hit a crossroads: do you want the whole thing in memory right now, or do you just want to move it from point A to point B? This is exactly what io.ReadAll and io.Copy are for. If you pick the wrong one, you're either writing more code than you need or, worse, you're writing a program that crashes the moment a user uploads a 5GB file.
To see this in action, I'm going to build a tiny "Backup Utility." The goal is simple: read a small config.txt file to see if backups are enabled, and if they are, stream a large system.log file into a backup.log file.
Grabbing the config with io.ReadAll
First, let's handle the configuration. Since I know config.txt is just a few lines of text, I don't need to worry about memory. I just want the bytes in a slice so I can convert them to a string and check the contents.
func checkConfig() (bool, error) {
f, err := os.Open("config.txt")
if err != nil {
return false, err
}
defer f.Close()
// io.ReadAll reads until EOF and returns a []byte
data, err := io.ReadAll(f)
if err != nil {
return false, err
}
return string(data) == "BACKUP_ENABLED=true", nil
}
io.ReadAll is great here. It's concise. It handles the loop of reading chunks for you and grows the slice as needed. If the file is small, this is the most ergonomic way to get the job done.
The mistake: Trying to ReadAll the logs
Now, let's move the actual log file. My first instinct—which is a habit I had to break early in my Go career—was to just use io.ReadAll again because it's so easy. I thought, "I'll just read the log file into a variable and then write that variable to the backup file."
// DON'T DO THIS for large files
data, err := io.ReadAll(logFile)
if err != nil {
return err
}
os.WriteFile("backup.log", data, 0644)
Here is the problem: io.ReadAll allocates a byte slice. If system.log is 10MB, no big deal. If it's 10GB, your program will try to allocate 10GB of RAM, the OS will panic, and your process will be killed with an "out of memory" (OOM) error. You should almost never use io.ReadAll on a file unless you have a strict guarantee about its maximum size.
Streaming the data with io.Copy
Instead of pulling the data into our program's memory, we can tell Go to just "pipe" it. io.Copy takes a Writer (the destination) and a Reader (the source) and moves data between them using a small, fixed-size buffer internally.
Here is how I'll actually implement the backup logic:
func performBackup() error {
source, err := os.Open("system.log")
if err != nil {
return err
}
defer source.Close()
destination, err := os.Create("backup.log")
if err != nil {
return err
}
defer destination.Close()
// io.Copy streams from source to destination.
// It doesn't load the whole file into RAM.
written, err := io.Copy(destination, source)
if err != nil {
return err
}
fmt.Printf("Successfully backed up %d bytes\n", written)
return nil
}
Notice the difference. I'm not creating a []byte slice. The data flows through a small window (usually 32KB) and goes straight to the disk. This means whether the log file is 1KB or 1TB, the memory usage of my program remains constant and tiny.
Choosing your tool
To wrap this up, here is my rule of thumb when I'm reviewing code:
- Use
io.ReadAllwhen you specifically need the entire content as a slice to manipulate it, and you know for a fact the input is small. - Use
io.Copywhen you are moving data from one place to another. It is safer, faster, and significantly more memory-efficient.
📋 Practical Task
Exercise: Build a Log Filter Streamer
Create a program that simulates a log filtering tool. Your program should:
- Read a small file named
filter.txtusingio.ReadAllto get a keyword (e.g., "ERROR"). - Open a larger file named
app.log. - Instead of copying the whole file, use a
bufio.Scannerto read the log file line-by-line. - If a line contains the keyword from
filter.txt, write that line to a new file calledfiltered.log. - Bonus: Use
io.Copyto copy the rest of theapp.logto afull_archive.logfile as a secondary backup step.
This will test your ability to distinguish between when to load a whole file into memory and when to stream data to avoid memory exhaustion.
There are no comments for now.