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
81: Fuzz Testing in Go
We've spent a lot of time on table-driven tests. They're the gold standard in Go for a reason: they're clean, easy to read, and they cover the "happy paths" and the edge cases you've already anticipated. But here is the cold truth: table-driven tests only prove that your code works for the inputs you were smart enough to think of. As someone who has spent a few too many weekends debugging production panics caused by a single unexpected null byte or a weird UTF-8 sequence, I can tell you that the "unknown unknowns" are where the real bugs hide.
The Trap of the Representative Sample
Imagine you've written a small utility function that parses a custom configuration string in the format key=value. You're feeling confident because you wrote a great test suite. It looks something like this:
func TestParseConfig(t *testing.T) {
tests := []struct {
input string
expected string
}{
{"port=8080", "8080"},
{"timeout=30s", "30s"},
{"", ""},
{"empty_val=", ""},
}
for _, tc := range tests {
got := ParseConfig(tc.input)
if got != tc.expected {
t.Errorf("ParseConfig(%q) = %q; want %q", tc.input, got, tc.expected)
}
}
}
This looks solid. You covered the empty string and the missing value. But this approach is fundamentally naive because it relies on your imagination. What happens if the input is "=value"? Or what if the input contains a null character, or a multi-byte emoji that throws off your index calculations? If you're using strings.Index and slicing the string based on that result without a sanity check, you're one weird input away from an index out of range panic that brings down your entire service.
Letting the Engine Find the Holes
This is where fuzzing comes in. Instead of you trying to guess every possible malicious or malformed string, you tell Go: "Here is the type of data I expect, and here is a property that should always hold true. Now, go try to break it."
In Go, we do this by switching from func TestXxx(t *testing.T) to func FuzzXxx(f *testing.F). I like to think of the f.Add calls as the "seed corpus"—you're giving the fuzzer a head start by providing the examples you already know about. Then, you provide a fuzz target function that the Go toolchain will call millions of times with mutated variations of those seeds.
func FuzzParseConfig(f *testing.F) {
// Seed corpus: give it some starting points
f.Add("port=8080")
f.Add("timeout=30s")
f.Add("")
f.Fuzz(func(t *testing.T, input string) {
// We aren't necessarily checking for a specific output here,
// but we are checking that the function NEVER panics.
ParseConfig(input)
})
}
When you run go test -fuzz=Fuzz, Go doesn't just throw random garbage at your function. It uses coverage-guided fuzzing. This means the engine tracks which code paths are being hit; if a specific mutation reaches a new branch of your if/else logic, the fuzzer recognizes that as "interesting" and starts mutating that specific input further to dig deeper into that branch. It's essentially an automated bug-hunter that doesn't get tired.
CPU Cycles vs. Sleep Quality
Now, I'm not suggesting you fuzz every single function in your codebase. Fuzzing is computationally expensive. It will pin your CPU to 100% and can run for hours, days, or indefinitely. If you're just adding two integers, a standard unit test is plenty.
However, if your code touches any of the following, you should be fuzzing:
- Parsing logic (JSON, XML, custom protocols).
- String manipulation that involves slicing or indexing.
- Decryption or decompression routines.
- Any input that comes directly from a user or an external API.
The trade-off is simple: you spend a few hours of CPU time now so you don't spend a few hours of your own life at 3 AM fixing a production crash. When the fuzzer finds a failure, it writes the exact input that caused the crash to a file in testdata/fuzz. You can then take that specific string, add it to your regular table-driven tests, fix the bug, and ensure it never regresses.
📋 Practical Task
Fixing the Fragile Log Parser
You have been handed a legacy function called ParseLogLevel that extracts the log level from a string (e.g., "[INFO] System started" should return "INFO"). The current implementation is brittle and crashes on certain inputs.
Your task:
- Implement a Fuzz test
FuzzParseLogLevel(f *testing.F)in a file namedparser_test.go. - Provide a seed corpus with at least three examples: a valid log line, an empty string, and a string with no brackets.
- Run the fuzzer using
go test -fuzz=Fuzzuntil it finds a crash (it will happen quickly). - Analyze the crashing input in
testdata, fix theParseLogLevelfunction to handle that case without panicking, and verify the fix by running the fuzzer again.
Starter Code:
package main
import "strings"
// ParseLogLevel extracts the content between the first set of square brackets.
// TODO: This is currently fragile!
func ParseLogLevel(input string) string {
start := strings.Index(input, "[")
end := strings.Index(input, "]")
return input[start+1 : end]
}
There are no comments for now.