Go Blank Identifier — Find the Bug¶
Instructions¶
Each exercise contains buggy Go code involving the blank identifier _. Identify the bug, explain the cause, and supply the fix. Difficulty: 🟢 Easy, 🟡 Medium, 🔴 Hard.
Bug 1 🟢 — Reading From _¶
package main
import "fmt"
func divmod(a, b int) (int, int) { return a / b, a % b }
func main() {
_, r := divmod(17, 5)
fmt.Println(_, r)
}
Solution
**Bug:** `fmt.Println(_, r)` tries to use `_` as a value. `_` is write-only. The compiler refuses: **Fix:** Bind the value to a real name: Or, if you really want to print only `r`, drop `_` from the print: **Key lesson:** `_` is a destination, not a name you can read.Bug 2 🟢 — Trying to Reference _ Across Statements¶
Solution
**Bug:** Two errors. First, `_ := 10` is illegal because short declaration `:=` requires at least one new non-blank name on the LHS. Second, even if the declarations were legal, each `_` is anonymous — there is no "the value of `_`" to print. If we replace with `var _ int = 10; var _ int = 20`, those compile, but `fmt.Println(_)` still fails: "cannot use _ as value". **Fix:** If you have two values to track, give them names: If you genuinely want both discarded: **Key lesson:** Each `_` is independent. There is no continuity between occurrences.Bug 3 🟢 — Forgotten Side-Effect Import (lib/pq)¶
package main
import (
"database/sql"
"fmt"
)
func main() {
db, err := sql.Open("postgres", "postgres://localhost/test?sslmode=disable")
if err != nil { panic(err) }
defer db.Close()
fmt.Println("connected")
}
The developer ran goimports, which removed _ "github.com/lib/pq" because it "wasn't used". Now sql.Open returns sql: unknown driver "postgres" (forgotten import?).
Solution
**Bug:** The blank import is the **mechanism** that registers the postgres driver. Removing it means `database/sql` has no driver under the name `"postgres"`. The error message even hints at the cause. **Fix:** Restore the import: **Prevention:** - Configure `goimports` (or your editor) to leave blank imports alone. - Add a comment so the next reader does not delete it: `// registers postgres driver`. - Ideally, group all driver imports in one place with a comment block. **Key lesson:** Side-effect imports look "unused" to naive tooling. They are not.Bug 4 🟡 — Compile-Time Assertion: Value vs Pointer Receiver¶
package main
import "fmt"
type Counter struct{ n int }
func (c *Counter) String() string {
return fmt.Sprintf("count=%d", c.n)
}
var _ fmt.Stringer = Counter{}
func main() {
c := Counter{n: 5}
fmt.Println(c)
}
Solution
**Bug:** The assertion `var _ fmt.Stringer = Counter{}` checks whether `Counter` (value type, not `*Counter`) implements `fmt.Stringer`. `String` has a pointer receiver, so only `*Counter` is a `fmt.Stringer`. Compile error: **Fix:** Assert against the pointer type: Now `*Counter` is checked, which has `String()`, and the line compiles. **Side note:** `fmt.Println(c)` will still call `String()` because `fmt` automatically takes the address when needed (when `c` is addressable, as a local variable is). But the assertion error is real and legitimate — it warns that you cannot use `Counter` (the value type) directly as a `fmt.Stringer` in interface contexts. **Key lesson:** When choosing between `T{}` and `(*T)(nil)` for assertions, follow the receiver. Pointer receiver → use `(*T)(nil)`. Value receiver → either form works (but `(*T)(nil)` is broader: a pointer type's method set includes both pointer- and value-receiver methods).Bug 5 🟡 — Receiver Discarded, Then Method "Calls" It¶
package main
import "fmt"
type Greeter struct{ name string }
func (_ *Greeter) Greet() {
fmt.Println("hello,", name) // intent: print g.name
}
func main() {
g := &Greeter{name: "Ada"}
g.Greet()
}
Solution
**Bug:** Two problems: 1. The method receiver is `_`, so the receiver value is unreachable inside the method. 2. The body references a free variable `name`, which is undefined at this scope. Compile error: The author probably wrote `_` for "I am too lazy to name the receiver" and forgot they would need it. The compile error is clear, but the deeper bug is the design — using `_` as a receiver when you actually need self-state is a contradiction. **Fix:** Name the receiver: **Key lesson:** Use `_` as a receiver only when the method genuinely does not use the receiver. If you need the data, name it.Bug 6 🟡 — Confusing _ with nil¶
package main
import "fmt"
type Logger interface {
Log(s string)
}
func use(l Logger) {
if l == _ {
fmt.Println("no logger")
return
}
l.Log("ok")
}
func main() {
use(nil)
}
Solution
**Bug:** `l == _` is not valid Go. `_` is not a value. The author confused `_` (the blank identifier) with `nil` (the typed-zero-value of a pointer or interface). The compiler refuses: **Fix:** Use `nil`: **Why people make this mistake:** Both `_` and `nil` represent "absence" in informal speech. They are completely different at the type-system level: - `_` is the destination "no name" — appears only on the LHS of assignments and a few other declaration positions. - `nil` is a value (typed zero) — appears in expressions wherever a pointer, interface, slice, map, channel, or function type is expected. **Key lesson:** `_` discards a write. `nil` is a value you compare against.Bug 7 🟡 — Multiple _ Pretending to Capture¶
package main
import "fmt"
func split(s string) (string, string, string) {
return s[:1], s[1:2], s[2:]
}
func main() {
_, _, last := split("abcdef")
fmt.Println(_, last) // intent: also print the second char
}
Solution
**Bug:** `fmt.Println(_, last)` references `_`, which is invalid. The author thought `_, _, last` "stored" the second value somewhere they could read. **Fix:** Bind the second value to a real name: **Variation people sometimes try:** This compiles only as an expression statement: `_, _, _ = split("abcdef")` (with `=`, not `:=`). With `:=`, you get "no new variables on left side of :=". **Key lesson:** If you need a value, name it. If you do not need it, `_` and forget about it.Bug 8 🟡 — Discarded Error Hides Failure¶
package main
import (
"encoding/json"
"fmt"
)
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
}
func loadConfig(data []byte) Config {
var c Config
_ = json.Unmarshal(data, &c) // discard error
return c
}
func main() {
cfg := loadConfig([]byte(`{"host": "localhost", "port": "not a number"}`))
fmt.Println(cfg)
}
Solution
**Bug:** `json.Unmarshal` returns an error when the JSON is malformed or types do not match. Discarding the error means the caller silently gets a `Config` with zero-valued fields where parsing failed (`Port` stays 0). The program "works" but produces wrong data. The user expected an error or a panic, not silent default-zeroing. **Fix:** Handle the error: **Key lesson:** `_ = json.Unmarshal(...)` is almost always a bug. Bad input is a real failure mode; the error message tells you why. The blank identifier should not be a way to mute legitimate failures.Bug 9 🟡 — Compile-Time Assertion in Wrong Package¶
// package main
package main
import (
"fmt"
"io"
"example.com/mylib"
)
var _ io.Reader = (*mylib.Reader)(nil) // assertion in main
func main() {
var r io.Reader = (*mylib.Reader)(nil)
fmt.Println(r)
}
Solution
**Bug:** This compiles, but the assertion is in the wrong place. The point of a compile-time interface assertion is to **catch breakage at the package that defines the type**. By putting it in `main`, you only catch breakage when `main` recompiles. If `mylib.Reader` loses its `Read` method, every consumer of `mylib` breaks before `main` does. The fix is to move the assertion **into the package that defines `Reader`**: Now any change to `mylib.Reader`'s method set fails to compile in `mylib` itself, before any consumer sees it. **Key lesson:** Compile-time assertions belong with the type definition. Putting them downstream defeats their purpose.Bug 10 🔴 — Side-Effect Import in a Library Package¶
// package mylib
package mylib
import (
"database/sql"
"fmt"
_ "github.com/lib/pq"
)
func Connect(dsn string) (*sql.DB, error) {
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("mylib.Connect: %w", err)
}
return db, nil
}
A consumer imports mylib and finds their binary now contains the postgres driver, even though they wanted to use mysql. Worse, the binary size grew by 2MB they did not ask for.
Solution
**Bug:** The library forces every consumer to link in `lib/pq`. This is a policy decision that belongs to the **binary** (`main` package), not to a library. **Fix 1:** Remove the blank import; let the consumer choose: **Fix 2:** Provide a sub-package per driver: The consumer imports the sub-package they want. **Key lesson:** Side-effect imports leak global state into every consumer. Keep them in `main` or in clearly-named opt-in sub-packages.Bug 11 🔴 — Reading "the value of _" From Loop¶
package main
import "fmt"
func main() {
nums := []int{10, 20, 30}
for _, v := range nums {
// ... do something with v
}
fmt.Println("last index:", _)
fmt.Println("last value:", v)
}
Solution
**Bug:** Two unrelated errors: 1. `_` cannot be read; `Println("last index:", _)` is invalid. 2. `v` is scoped to the `for` loop; outside the loop it does not exist. The author may have come from a language where loop variables persist after the loop, or where `_` could be inspected. **Fix:** Capture what you want into outer-scoped variables: **Key lesson:** `_` does not store the value, and loop scope ends at the closing brace.Bug 12 🔴 — Mistaking _ for an Importable Name¶
Solution
**Bug:** The author thinks `_` is the package alias and they can call `_.Helper`. It is not. `import _ "example.com/utils"` means **"do not bind any name from this package"**. There is no package value to dereference. **Fix:** If you want to call functions from `utils`, import it normally: If `utils` is needed only for its `init` side effect, the blank import is correct, but you cannot then call into the package. **Key lesson:** Blank imports give you NOTHING from the package's namespace. They are for the side effect only.Bug 13 🔴 — Discarded Returns Across Layers¶
package main
import "fmt"
func computeAndCheck() (int, error) {
return 0, fmt.Errorf("bad input")
}
func wrapper() int {
n, _ := computeAndCheck()
return n
}
func main() {
fmt.Println(wrapper()) // 0 — caller cannot tell why
}
The caller of wrapper cannot distinguish "n=0 is a valid answer" from "n=0 because the call failed".
Solution
**Bug:** `wrapper` swallowed the error. The bug is not at the `_` — it is at the design of `wrapper`. `wrapper` lost information. **Fix:** Propagate: Or handle: **Key lesson:** Discarding an error inside a wrapper hides the failure from callers. If the caller cannot proceed safely without knowing about the error, propagate it.Bug 14 🔴 — Padding Field Position Mistake¶
package main
import (
"fmt"
"unsafe"
)
type Counter struct {
_ [56]byte // padding before A?
A uint64
B uint64
}
func main() {
c1 := Counter{}
c2 := Counter{}
fmt.Println(unsafe.Sizeof(c1), &c1.A, &c1.B, &c2.A)
}
The author intended to put A and B on separate cache lines to avoid false sharing. They added padding before A instead of between A and B. The fields A and B still share a cache line.
Solution
**Bug:** The padding `_ [56]byte` is at offset 0; `A` follows at offset 56; `B` follows at offset 64. Although `A` and `B` are 8 bytes apart, they may STILL straddle a cache-line boundary depending on the alignment of `c1` itself. Worse, every instance has 56 bytes of wasted space at the start. **Fix:** Pad **between** the fields: Now offset of `A` is 0, offset of `B` is 64. They are guaranteed on different cache lines (assuming `c1` itself is at least 8-byte aligned, which it is). **Key lesson:** Padding placement matters. Put padding between hot fields, not before them. The cache-line size on x86 is 64 bytes; on ARM64 (Apple silicon) it is often 128.Bug 15 🔴 — Compile-Time Assertion with Generic Constraint¶
package main
type Container[T any] interface {
Get() T
}
type IntBox struct{ v int }
func (b *IntBox) Get() string { return "" }
var _ Container[int] = (*IntBox)(nil)
func main() {}
Solution
**Bug:** `IntBox.Get` returns `string`, but `Container[int]` requires `Get() int`. The compile-time assertion exists precisely to catch this; the compiler reports: The bug is in the `Get` method, not the assertion. The assertion correctly flags the mismatch. **Fix:** Either correct the method signature to match the intended interface: …or change the interface parameterization to match the type: …depending on what `IntBox` is actually meant to do. **Key lesson:** This is the assertion working as intended — catching a bug at compile time. The "bug" is that without the assertion, the mismatch would surface later, possibly at runtime.16. Summary of Common Bug Categories¶
- Reading from
_— Always a compile error. - Treating
_as a name across statements — There is no continuity. - Removing a side-effect import thinking it is unused — Drivers and decoders register via
init. - Wrong receiver kind in compile-time assertions — Use
(*T)(nil)for pointer-receiver methods. _shadowing illusion —_cannot be shadowed because it is not a binding.- Receiver discard then accessing self — Pick named receiver if you need it.
- Confusing
_withnil— They are different categories. - Discarding errors that mattered — Especially
json.Unmarshal,os.Removewhen correctness matters. - Side-effect import in a library package — Belongs in
main. - Padding placement mistakes — Pad between hot fields, not at the start.
If you see _ in a code review, ask: "Why is this discarded?" If the author cannot answer cleanly, the _ probably hides a bug.