Structs — Find the Bug¶
Each exercise contains a bug. Identify it, explain the problem, and provide the fix.
Difficulty: 🟢 Easy | 🟡 Medium | 🔴 Hard
Bug 1 🟢 — The Copy Mutation¶
package main
import "fmt"
type Rectangle struct {
Width float64
Height float64
}
func doubleSize(r Rectangle) {
r.Width *= 2
r.Height *= 2
fmt.Println("Inside:", r.Width, r.Height)
}
func main() {
rect := Rectangle{Width: 5, Height: 3}
doubleSize(rect)
fmt.Println("Outside:", rect.Width, rect.Height)
// Expected: Outside: 10 6
// Got: ???
}
Bug Explanation & Fix
**Bug:** `doubleSize` takes a value receiver (copy). Modifications to `r` inside the function don't affect the original `rect`. The caller's struct is unchanged. **Output:** **Fix:**// Option 1: pointer receiver
func doubleSize(r *Rectangle) {
r.Width *= 2
r.Height *= 2
}
func main() {
rect := Rectangle{Width: 5, Height: 3}
doubleSize(&rect)
fmt.Println("Outside:", rect.Width, rect.Height) // 10 6
}
// Option 2: return new value (functional style)
func doubleSize(r Rectangle) Rectangle {
r.Width *= 2
r.Height *= 2
return r
}
func main() {
rect := Rectangle{Width: 5, Height: 3}
rect = doubleSize(rect) // explicitly reassign
fmt.Println(rect.Width, rect.Height) // 10 6
}
Bug 2 🟢 — The Nil Pointer Dereference¶
package main
import "fmt"
type Node struct {
Value int
Next *Node
}
func printList(n *Node) {
for n != nil {
fmt.Println(n.Value)
n = n.Next
}
}
func main() {
n1 := Node{Value: 1}
n2 := Node{Value: 2}
n1.Next = &n2
// Add third node
n3 := &Node{Value: 3}
n2.Next = n3
// Try to print 4th value
fmt.Println(n3.Next.Value) // BUG
printList(&n1)
}
Bug Explanation & Fix
**Bug:** `n3.Next` is `nil` (n3 has no next node). Accessing `n3.Next.Value` is a nil pointer dereference — **panic at runtime**. **Fix:** **General rule:** Always check `*T != nil` before accessing fields through a pointer, especially for user-provided data or linked data structures.Bug 3 🟢 — Positional Struct Literal Breakage¶
package main
import "fmt"
// Version 1 of the struct
type Config struct {
Host string
Port int
}
func newDefaultConfig() Config {
return Config{"localhost", 8080} // positional initialization
}
// Later, someone adds a field:
// type Config struct {
// Host string
// Port int
// Timeout int // NEW FIELD
// }
func main() {
cfg := newDefaultConfig()
fmt.Println(cfg.Host, cfg.Port)
}
Bug Explanation & Fix
**Bug:** Using positional struct literals (`Config{"localhost", 8080}`) is fragile. When a new field `Timeout` is added, `Config{"localhost", 8080}` becomes `Config{Host: "localhost", Port: 8080, Timeout: 0}` — but the compile error appears because the literal now has too few arguments for the positional form. Worse: if fields are reordered, `Port` gets "localhost" (a compile error if types differ, but a silent bug if compatible). **Fix: Always use named field initialization** Named literals are immune to field additions and reordering. The `go vet` tool warns about composite literal using fields from a different type definition.Bug 4 🟡 — The Shared Slice in Copied Struct¶
package main
import "fmt"
type Team struct {
Name string
Members []string
}
func main() {
team1 := Team{
Name: "Alpha",
Members: []string{"Alice", "Bob"},
}
// Create team2 as a copy of team1, then rename it
team2 := team1
team2.Name = "Beta"
team2.Members = append(team2.Members, "Carol")
fmt.Println("Team1:", team1.Name, team1.Members)
fmt.Println("Team2:", team2.Name, team2.Members)
// Expected:
// Team1: Alpha [Alice Bob]
// Team2: Beta [Alice Bob Carol]
// Actual: ???
}
Bug Explanation & Fix
**Bug:** When `team2 := team1`, the `Members` slice header is copied — but both headers point to the SAME underlying array. The `append` in `team2.Members = append(team2.Members, "Carol")` may or may not cause a problem: - If the underlying array has capacity: `Carol` is written to the shared array, `team1.Members` is unaffected in terms of its length/pointer, but the data at `team1.Members[2]` (beyond team1's length) now exists. No visible bug here since team1's length is still 2. - BUT: if the slice had capacity 2 (len=cap=2), `append` allocates a new array — team2 gets its own copy, team1 is unaffected. This works correctly. The actual bug is more subtle: `team2.Members[0] = "Eve"` would modify team1's members too: **Fix: Deep copy the slice**Bug 5 🟡 — The Value Method Trying to Modify State¶
package main
import "fmt"
type Counter struct {
count int
name string
}
func (c Counter) Increment() {
c.count++ // tries to increment
}
func (c Counter) GetCount() int {
return c.count
}
func main() {
c := Counter{name: "page_views"}
c.Increment()
c.Increment()
c.Increment()
fmt.Println(c.name, ":", c.GetCount()) // Expected: 3, Got: ???
}
Bug Explanation & Fix
**Bug:** `Increment` uses a value receiver — it receives a copy of `Counter`. `c.count++` increments the copy, which is discarded when the method returns. The original `c.count` is never changed. **Output:** `page_views : 0` — always 0! **Fix:** **Rule:** If a method modifies fields, it MUST use a pointer receiver.Bug 6 🟡 — The Typed Nil Return¶
package main
import "fmt"
type DBError struct {
Code int
Message string
}
func (e *DBError) Error() string {
return fmt.Sprintf("DB[%d]: %s", e.Code, e.Message)
}
func queryUser(id int) *DBError {
if id <= 0 {
return &DBError{Code: 400, Message: "invalid id"}
}
// success — no error
return nil
}
func main() {
var err error = queryUser(1) // assign *DBError to error interface
if err != nil {
fmt.Println("Error:", err) // Does this print?
} else {
fmt.Println("Success")
}
}
Bug Explanation & Fix
**Bug:** `queryUser(1)` returns `nil` (typed `*DBError`). When this is assigned to `var err error`, the interface value becomes `{type: *DBError, value: nil}` — which is **NOT a nil interface**. So `err != nil` is `true`, and `fmt.Println("Error:", err)` prints `"Error:Bug 7 🟡 — The Mutex Copy¶
package main
import (
"fmt"
"sync"
)
type SafeMap struct {
mu sync.Mutex
data map[string]int
}
func NewSafeMap() SafeMap {
return SafeMap{data: make(map[string]int)}
}
func (sm SafeMap) Set(key string, val int) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.data[key] = val
}
func (sm SafeMap) Get(key string) (int, bool) {
sm.mu.Lock()
defer sm.mu.Unlock()
v, ok := sm.data[key]
return v, ok
}
func main() {
m := NewSafeMap()
m.Set("a", 1)
v, ok := m.Get("a")
fmt.Println(v, ok) // Expected: 1 true — but what actually happens?
}
Bug Explanation & Fix
**Bug:** Three issues: 1. `Set` uses a value receiver — receives a COPY of SafeMap including the mutex. Modifications to `sm.data["a"] = val` modify the copied map header... but `sm.data` and `m.data` share the same underlying hash table, so the write works. However, the mutex protection is on a COPY — the original mutex `m.mu` is never locked! 2. `Get` has the same value receiver problem with mutex. 3. `go vet` reports: "Set passes lock by value: SafeMap contains sync.Mutex" The real danger: two goroutines calling `Set` concurrently will both use different copies of the mutex — both can Lock() simultaneously, causing data race on the map. **Fix:**type SafeMap struct {
mu sync.Mutex
data map[string]int
}
func NewSafeMap() *SafeMap { // return pointer!
return &SafeMap{data: make(map[string]int)}
}
func (sm *SafeMap) Set(key string, val int) { // pointer receiver
sm.mu.Lock()
defer sm.mu.Unlock()
sm.data[key] = val
}
func (sm *SafeMap) Get(key string) (int, bool) { // pointer receiver
sm.mu.Lock()
defer sm.mu.Unlock()
v, ok := sm.data[key]
return v, ok
}
Bug 8 🟡 — Map Value Not Addressable¶
package main
import "fmt"
type Stats struct {
Count int
Total float64
}
func (s *Stats) AddSample(v float64) {
s.Count++
s.Total += v
}
func main() {
statsByKey := map[string]Stats{
"requests": {Count: 0, Total: 0},
}
// Record some requests
statsByKey["requests"].AddSample(1.5)
statsByKey["requests"].AddSample(2.0)
fmt.Println(statsByKey["requests"])
}
Bug Explanation & Fix
**Bug:** `statsByKey["requests"]` returns a non-addressable value. You cannot call a pointer-receiver method on it. This is a **compile error**: Even if you could get a pointer, any modification wouldn't persist back to the map. **Fix 1: Store pointers in the map** **Fix 2: Read-modify-write** **Fix 3: Make AddSample a value receiver that returns a new value**Bug 9 🔴 — The Struct Embedding Method Shadow¶
package main
import "fmt"
type Base struct {
ID int
}
func (b Base) Describe() string {
return fmt.Sprintf("Base{ID: %d}", b.ID)
}
type Extended struct {
Base
Name string
}
func (e Extended) Describe() string {
return fmt.Sprintf("Extended{ID: %d, Name: %s}", e.ID, e.Name)
}
type Printer interface {
Describe() string
}
func printAll(items []Printer) {
for _, item := range items {
fmt.Println(item.Describe())
}
}
func main() {
items := []Printer{
Base{ID: 1},
Extended{Base: Base{ID: 2}, Name: "Alice"},
}
printAll(items)
// Now something subtle:
bases := []Base{
{ID: 10},
Extended{Base: Base{ID: 20}, Name: "Bob"}.Base, // extract just the Base
}
for _, b := range bases {
fmt.Println(b.Describe()) // What does this print for both?
}
}
Bug Explanation & Fix
**Bug/Tricky behavior:** This isn't a compile error, but demonstrates a subtle Go behavior. `Extended{...}.Base` extracts just the `Base` part of the Extended struct. When you iterate `bases` and call `b.Describe()`, you always call `Base.Describe()` — not `Extended.Describe()` — because the slice holds `Base` values, not `Extended`. This is **Go's non-polymorphic value semantics**. Unlike Java/Python, Go does NOT have virtual dispatch. When you extract `Extended.Base`, you lose the `Extended` type information. **Output:** **Fix for polymorphic behavior: use interfaces** **Key insight:** In Go, "polymorphism" only happens through interfaces, not through struct embedding.Bug 10 🔴 — The Field Alignment Panic on 32-bit¶
package main
import (
"fmt"
"sync/atomic"
)
type Metrics struct {
name string
counter int64 // needs 8-byte alignment
active bool
}
type Server struct {
host string
metrics Metrics // metrics.counter may not be 8-byte aligned!
}
func (m *Metrics) Increment() {
atomic.AddInt64(&m.counter, 1)
}
func main() {
s := Server{
host: "localhost",
metrics: Metrics{name: "requests"},
}
s.metrics.Increment()
fmt.Println("Count:", s.metrics.counter)
}
Bug Explanation & Fix
**Bug:** On 32-bit platforms (ARM, x86), `atomic.AddInt64` requires the `int64` field to be 8-byte aligned. The actual offset of `metrics.counter` within `Server` depends on the layout:Server.host: offset 0 (string: ptr+len = 8 bytes on 32-bit)
Server.metrics: offset 8
metrics.name: offset 8 (ptr+len = 8 bytes)
metrics.counter: offset 16 (8-byte aligned — OK on this layout)
metrics.active: offset 24
// Place int64 field first — guaranteed 8-byte aligned
type Metrics struct {
counter int64 // FIRST — always 8-byte aligned
name string
active bool
}
// Or: use sync/atomic.Int64 (Go 1.19+) which handles alignment internally
import "sync/atomic"
type Metrics struct {
counter atomic.Int64 // built-in alignment guarantee
name string
active bool
}
func (m *Metrics) Increment() {
m.counter.Add(1)
}
Bug 11 🔴 — The Interface Embedding Nil Panic¶
package main
import (
"fmt"
"io"
)
type BufferedReader struct {
io.Reader // embedded interface — nil by default!
buf []byte
}
func NewBufferedReader(r io.Reader) *BufferedReader {
if r == nil {
return &BufferedReader{} // forgot to set Reader field!
}
return &BufferedReader{Reader: r, buf: make([]byte, 4096)}
}
func (br *BufferedReader) ReadAll() ([]byte, error) {
if len(br.buf) == 0 {
br.buf = make([]byte, 4096)
}
n, err := br.Read(br.buf) // calls embedded Reader.Read()
if err == io.EOF {
return br.buf[:n], nil
}
return br.buf[:n], err
}
func main() {
// Case 1: valid reader
br := NewBufferedReader(strings.NewReader("hello"))
data, err := br.ReadAll()
fmt.Println(string(data), err)
// Case 2: nil reader passed
br2 := NewBufferedReader(nil) // creates BufferedReader with nil Reader
data2, err2 := br2.ReadAll() // PANIC
fmt.Println(string(data2), err2)
}
Bug Explanation & Fix
**Bug:** When `r == nil`, `NewBufferedReader` returns `&BufferedReader{}` with the embedded `io.Reader` field as `nil`. Calling `br.Read(br.buf)` (promoted from embedded Reader) panics with "nil pointer dereference" because it tries to call the `Read` method on a nil interface. **Fix:**func NewBufferedReader(r io.Reader) (*BufferedReader, error) {
if r == nil {
return nil, errors.New("reader must not be nil")
}
return &BufferedReader{Reader: r, buf: make([]byte, 4096)}, nil
}
// Or: add a nil guard in ReadAll
func (br *BufferedReader) ReadAll() ([]byte, error) {
if br.Reader == nil {
return nil, errors.New("no reader configured")
}
if len(br.buf) == 0 {
br.buf = make([]byte, 4096)
}
n, err := br.Read(br.buf)
if err == io.EOF {
return br.buf[:n], nil
}
return br.buf[:n], err
}
Bug 12 🔴 — The Concurrent Map Struct with Wrong Mutex Scope¶
package main
import (
"fmt"
"sync"
)
type UserCache struct {
users map[string]User
mu sync.RWMutex
}
type User struct {
Name string
Email string
}
func (c *UserCache) GetOrCreate(name string) *User {
c.mu.RLock()
if u, ok := c.users[name]; ok {
c.mu.RUnlock()
return &u // BUG: returning pointer to local variable!
}
c.mu.RUnlock()
newUser := User{Name: name, Email: name + "@example.com"}
c.mu.Lock()
// BUG: double-check missing — another goroutine may have inserted between RUnlock and Lock!
c.users[name] = newUser
c.mu.Unlock()
return &newUser // BUG: returning pointer to local variable!
}
Bug Explanation & Fix
**Bug 1:** `return &u` — `u` is a local copy of the map value (struct copy). The pointer `&u` points to a stack variable that is invalid after the function returns. **Bug 2:** After `c.mu.RUnlock()` and before `c.mu.Lock()`, another goroutine may have already inserted the same user. Without a double-check under the write lock, we'll overwrite the existing user. **Bug 3:** `return &newUser` — same issue as Bug 1: returning pointer to local variable. **Fix:**type UserCache struct {
users map[string]*User // store pointers to avoid copy issues
mu sync.RWMutex
}
func (c *UserCache) GetOrCreate(name string) *User {
// Check with read lock first
c.mu.RLock()
if u, ok := c.users[name]; ok {
c.mu.RUnlock()
return u // safe: returning stored pointer
}
c.mu.RUnlock()
// Upgrade to write lock with double-check
c.mu.Lock()
defer c.mu.Unlock()
// Double-check: another goroutine may have added it
if u, ok := c.users[name]; ok {
return u
}
u := &User{Name: name, Email: name + "@example.com"}
c.users[name] = u
return u // safe: storing and returning the same pointer
}