Go fmt — Find the Bug¶
Instructions¶
Each exercise contains buggy Go code. Identify the bug, explain why, and provide the corrected code. Difficulty: 🟢 Easy, 🟡 Medium, 🔴 Hard.
Bug 1 🟢 — %d on a String¶
Solution
`%d` is the integer verb but `name` is a `string`. Output: `go vet` warns at build time: Fix: `fmt.Printf("Hello, %s\n", name)` (or `%v`). `vet` catches verb/argument mismatches. Run it in CI; treat warnings as errors.Bug 2 🟢 — %s on a Struct With No Stringer¶
Solution
`%s` on a struct without `String()` falls back to a default format: Fix — choose your representation: For types you print or log, define `String()`.Bug 3 🟢 — %v on a Nil Interface¶
Solution
Output: `error:Bug 4 🟢 — %w in Sprintf¶
Solution
Output: `outer: %!w(*errors.errorString=&{inner})`. `%w` is recognised **only** by `fmt.Errorf`. In `Sprintf` it falls back to a malformed-verb placeholder, and there's no chain — the result is a string, not an error. Fix: `go vet` and `errorlint` both catch this.Bug 5 🟡 — Stringer Infinite Recursion¶
type M struct{ X, Y int }
func (m M) String() string { return fmt.Sprintf("%v", m) }
fmt.Println(M{1, 2})
Solution
`%v` of `M` calls `M.String()`, which calls `Sprintf("%v", m)`, which calls `String()` again. Stack overflow: Fix 1 — explicit field references: Fix 2 — alias trick (strips the method): `vet` does NOT catch this. Add a unit test that calls `String()`.Bug 6 🟡 — Pointer Receiver Stringer on Value¶
type T struct{ V int }
func (t *T) String() string { return fmt.Sprintf("T(%d)", t.V) }
t := T{V: 42}
fmt.Println(t) // {42}
fmt.Println(&t) // T(42)
Solution
`String()` has a pointer receiver. `T` (a value) doesn't implement `Stringer`; only `*T` does. So `fmt.Println(t)` falls back to the default `{42}`. Fix — value receiver: Define `String()` on the value receiver unless the type is meant to be used only by pointer (rare for small structs). Same rule applies to `Format` and `GoString`.Bug 7 🟡 — Width Modifier Misuse¶
Solution
Output: `%!d(string=hello)`. Width applies only after type-checking. For strings, width is min chars and precision is max chars: Width and precision have different meanings per verb. Read the verb table.Bug 8 🟢 — Forgetting to Escape %¶
Solution
Output: `100%!(NOVERB)`. `%\n` parses as a malformed verb. Fix: double the `%`: `fmt.Printf("100%%\n")` → `100%`. `vet` catches this.Bug 9 🟡 — Println in a Hot Loop¶
Solution
`Println` has hidden costs in tight loops: 1. Allocating an `[]any` for the variadic args. 2. Boxing `v` into an `any` (alloc if outside small-int range). 3. The `os.Stdout` mutex lock per call. 4. Kernel write per line (no buffering by default). For 1M iterations: ~10s, ~3M allocs, ~200 MB of GC pressure. Fix 1 — buffered writer: Fix 2 — `slog.Debug` (zero-alloc handler). Fix 3 — don't log in a hot loop. Aggregate first. `fmt.Println` is interactive-output speed; 1M/sec is wrong tooling.Bug 10 🟡 — %q / %v on Bytes vs Strings¶
Solution
Both produce `"hello\nworld"` — `%q` treats `[]byte` and `string` identically (both go through `strconv.Quote`). The actual gotcha is `%v` on `[]byte`: Code that does `%v` on `[]byte` expecting the string is a common bug. Use `%s` or convert with `string(b)`. Hex variants:Bug 11 🟡 — Custom Error That Loses %w¶
type AppError struct{ Op string; Err error }
func (e *AppError) Error() string {
return fmt.Sprintf("%s: %v", e.Op, e.Err)
}
_, ioErr := os.Open("/no/such")
err := &AppError{Op: "load", Err: ioErr}
fmt.Println(errors.Is(err, fs.ErrNotExist)) // false
Solution
`AppError` doesn't implement `Unwrap()`. `errors.Is` walks the chain via `Unwrap`; without it, the chain ends at `AppError`. Fix: After: `errors.Is(err, fs.ErrNotExist)` returns `true`. A custom error that wraps another **must** implement `Unwrap()`, or use `fmt.Errorf("...: %w", inner)` directly.Bug 12 🔴 — User Input as Format String¶
Solution
`msg` is user-controlled. Passing `%s %d %x %v` causes `fmt` to look for arguments and emit `%!s(MISSING)` etc. Not memory-unsafe in Go (unlike C), but: - Leaks verbose output the developer didn't intend. - User-controlled formatting in logs. - Confuses log parsers. Fix — `Print` for literal output, or `%s` to constrain: `staticcheck SA1006` catches this. Format strings must be constants.Bug 13 🟡 — Missing Argument Silent¶
Solution
Output: `user=ada id=%!d(MISSING)`. `fmt` doesn't error — it inserts a placeholder and continues, so the bug may slip into logs. `vet` catches it at compile time: Always run `vet`; always fix `printf` warnings.Bug 14 🔴 — fmt.Errorf With Multiple %w and Nil¶
var cleanupErr error // nil
primary := errors.New("primary")
err := fmt.Errorf("step: %w; cleanup: %w", primary, cleanupErr)
Solution
Since Go 1.20, `Errorf` panics if any argument bound to `%w` is `nil`: Fix — guard, or use `errors.Join` (silently ignores nil): Multiple `%w` requires non-nil errors. `errors.Join` is safer for collected errors.Bug 15 🟡 — Println Adds Spaces Where You Don't Want¶
Solution
`Println` always adds a space between operands. Fix — `Printf` for control: Subtle: `Print` only adds spaces between two non-string args:Bug 16 🔴 — Format That Panics on nil¶
type N struct{ V int }
func (n *N) String() string { return fmt.Sprintf("N(%d)", n.V) }
var p *N
fmt.Println(p)
Solution
`String()` dereferences `n.V` without nil-checking. `fmt.Println(p)` with `p == nil` calls `(*N)(nil).String()`, which nil-derefs. `fmt` recovers from panics inside `String()` and prints: Don't rely on this. Nil-check inside `String()`: Pointer-receiver methods that may be called on nil must nil-check.Summary: 10 Mandated Bugs Coverage¶
| # | Bug | Where |
|---|---|---|
| 1 | %d on string | Bug 1 |
| 2 | %s on non-Stringer struct | Bug 2 |
| 3 | %v on nil interface | Bug 3 |
| 4 | %w in Sprintf | Bug 4 |
| 5 | Stringer infinite recursion | Bug 5 |
| 6 | Pointer-receiver Stringer on value | Bug 6 |
| 7 | Width modifier misuse | Bug 7 |
| 8 | Forgetting to escape % | Bug 8 |
| 9 | Println in hot loop | Bug 9 |
| 10 | %q/%v on bytes vs strings | Bug 10 |
Plus 6 bonus production traps: missing Unwrap, format-string injection, missing argument, multiple %w with nil, Println spacing, nil-receiver String panic.
First line of defense: go vet. Second: errorlint and staticcheck. Third: a habit of running every new String() through a fmt.Println test before shipping.