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
165: Practice Exercise: Building a Simple CLI Log Viewer
I've spent a lot of my career digging through massive `.log` files on remote servers. Usually, I just use grep or tail, but there are times when you need a custom tool—maybe something that parses a specific weird timestamp format or filters by a complex set of rules—where a quick Go utility is the way to go.
Let's build a simple CLI log viewer together. I'm not going to give you a finished blueprint; instead, let's just start coding and see where we hit walls.
The "Easy Way" that fails at scale
My first instinct when reading a file is usually os.ReadFile. It's simple: you give it a path, it gives you a byte slice. Let's try that.
package main
import (
"fmt"
"os"
"strings"
)
func main() {
data, _ := os.ReadFile("server.log")
lines := strings.Split(string(data), "\n")
for _, line := range lines {
if strings.Contains(line, "ERROR") {
fmt.Println(line)
}
}
}
This works perfectly for a 10KB file. But here's the problem: log files in the real world are often gigabytes in size. If I run this against a 4GB production log, Go is going to try to allocate 4GB of RAM immediately, and the OS is likely to kill my process. We're treating the log like a single string, which is a classic rookie mistake in systems programming.
Switching to a stream
We need to process the file line by line without loading the whole thing into memory. This is where bufio.Scanner comes in. It's like a sliding window moving through the file.
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
file, err := os.Open("server.log")
if err != nil {
fmt.Println("Error opening file:", err)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, "ERROR") {
fmt.Println(line)
}
}
}
This is much better. The memory footprint stays flat regardless of the file size. But wait—I just noticed something. If the log file has an incredibly long line (some JSON logs can be massive), bufio.Scanner will actually fail because it has a default buffer limit (usually 64KB). If we hit a line longer than that, scanner.Scan() returns false and stops silently.
I always forget to check the error after the loop. Let's add that so we aren't flying blind.
for scanner.Scan() {
// ... processing ...
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading standard input:", err)
}
Adding a bit of flexibility
Hardcoding "ERROR" and "server.log" makes this a script, not a tool. I want to be able to search for different terms and target different files. I'll use the flag package. I've found that keeping flags simple is better than over-engineering a complex CLI parser for a small utility.
package main
import (
"bufio"
"flag"
"fmt"
"os"
"strings"
)
func main() {
// Define our flags
filePath := flag.String("file", "", "Path to the log file")
searchTerm := flag.String("search", "ERROR", "Term to filter by")
flag.Parse()
if *filePath == "" {
fmt.Println("Please provide a file path using -file")
os.Exit(1)
}
file, err := os.Open(*filePath)
if err != nil {
fmt.Printf("Could not open %s: %v\n", *filePath, err)
os.Exit(1)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, *searchTerm) {
fmt.Println(line)
}
}
if err := scanner.Err(); err != nil {
fmt.Printf("Error reading file: %v\n", err)
}
}
Now we have a tool that streams data, handles memory efficiently, warns us if the buffer overflows, and takes user input. It's not grep, but it's a solid foundation for a custom log processor.
📋 Practical Task
Exercise: Build a Log Viewer with Line Limiting
Your task is to extend the log viewer logic we just explored. Create a CLI tool that allows a user to view logs with a specific limit on how many matching lines are returned.
Requirements:
- Implement a
-fileflag (string) for the log path. - Implement a
-searchflag (string) for the keyword. - Implement a
-limitflag (int) that stops the program once it has found X number of matching lines. If the limit is 0 or negative, it should show all matches. - Use
bufio.Scannerto ensure the tool can handle files larger than the available system RAM. - Ensure you check for
scanner.Err()after the loop finishes.
Test Case:
If you have a file with 100 "ERROR" lines and you run go run main.go -file app.log -search ERROR -limit 10, the output should contain exactly 10 lines and then exit.
There are no comments for now.