Methods vs Functions — Find the Bug¶
Each exercise follows this format: 1. Buggy code 2. Hint 3. Identifying the bug and its cause 4. Fixed code
Bug 1 — Counter not modified¶
type Counter struct{ n int }
func (c Counter) Inc() { c.n++ }
func main() {
c := Counter{}
c.Inc()
c.Inc()
c.Inc()
fmt.Println(c.n) // ?
}
Hint: Examine Inc's receiver carefully.
Bug: Inc uses a value receiver — when c is passed to Inc, a copy is made. c.n++ modifies only the copy, leaving the original c unaffected. Output: 0.
Fix:
Bug 2 — Map element pointer receiver¶
type User struct{ name string }
func (u *User) Rename(n string) { u.name = n }
func main() {
users := map[string]User{"a": {name: "Alice"}}
users["a"].Rename("Alicia") // ?
}
Hint: Where is the compile error?
Bug: Map elements are not addressable — you cannot take &users["a"], so a pointer receiver method cannot be called on it. Compile error: cannot call pointer method on map value.
Fix:
Or change the map to map[string]*User.
Bug 3 — Mutex copied¶
type Counter struct {
mu sync.Mutex
n int
}
func (c Counter) Inc() { // value receiver
c.mu.Lock()
defer c.mu.Unlock()
c.n++
}
Hint: Race condition + mutex.
Bug: A value receiver copies c, and the mutex gets copied along with it. Concurrent goroutines each Lock their own copy of the mutex — there is no synchronization. go vet issues a "passes lock by value" warning.
Fix:
Bug 4 — Method set mismatch¶
type Greeter interface { Greet() string }
type Person struct{ name string }
func (p *Person) Greet() string { return "Hi " + p.name }
func main() {
p := Person{name: "Alice"}
var g Greeter = p
fmt.Println(g.Greet())
}
Hint: Check the method set rules.
Bug: Greet has a pointer receiver — it is not in Person's method set. A Person value cannot be assigned to the Greeter interface. Compile error: Person does not implement Greeter (Greet method has pointer receiver).
Fix:
Or switch to a value receiver: func (p Person) Greet() string { ... }.
Bug 5 — Nil pointer panic¶
type Logger struct{ prefix string }
func (l *Logger) Log(msg string) {
fmt.Println(l.prefix, msg)
}
func main() {
var l *Logger
l.Log("hi")
}
Hint: What happens when the receiver is nil?
Bug: l is nil. Dereferencing l.prefix causes a runtime panic: nil pointer dereference.
Fix (defensive):
Or make the constructor mandatory: NewLogger(prefix string) *Logger.
Bug 6 — Closure receiver¶
type Service struct{ id int }
func (s *Service) Process() { fmt.Println("service", s.id) }
func main() {
services := []*Service{{1}, {2}, {3}}
callbacks := []func(){}
for _, s := range services {
callbacks = append(callbacks, s.Process)
}
for _, cb := range callbacks { cb() }
}
Hint: Go 1.21 and 1.22 differ.
Bug: In Go 1.21 and earlier, s is the same variable in every iteration — the s.Process method value binds to the final s each time. Output: service 3 × 3.
In Go 1.22+ a new s is created each iteration — expected output: 1, 2, 3.
Fix (1.21 and earlier):
for _, s := range services {
s := s // shadowing — fresh copy per iteration
callbacks = append(callbacks, s.Process)
}
Bug 7 — Using slice with struct copy¶
type Stack struct{ items []int }
func (s Stack) Push(x int) { s.items = append(s.items, x) }
func main() {
s := Stack{}
s.Push(1); s.Push(2); s.Push(3)
fmt.Println(s.items) // ?
}
Hint: The behavior of append.
Bug: Value receiver — s is a copy. append either adds an element to the slice's underlying array or allocates a new array, but the change to s.items is preserved only in the local copy. The original Stack does not change. Output: [].
Fix:
Bug 8 — Goroutine receiver¶
type Server struct{ count int }
func (s *Server) Handle() { s.count++ }
func main() {
s := &Server{}
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
s.Handle()
}()
}
wg.Wait()
fmt.Println(s.count) // ?
}
Hint: Race condition.
Bug: s.count++ is not atomic. Race condition — the final count may be less than 1000. go run -race will detect it.
Fix:
import "sync/atomic"
type Server struct{ count atomic.Int64 }
func (s *Server) Handle() { s.count.Add(1) }
// ...
fmt.Println(s.count.Load())
Or use sync.Mutex.
Bug 9 — Method declaration on alias¶
Hint: Defined type vs alias.
Bug: type Time = time.Time is an alias (with =). time.Time lives in another package — methods cannot be added to an alias. Compile error: cannot define new methods on non-local type time.Time.
Fix:
Bug 10 — Embedded interface conflict¶
type Reader interface { Read() string }
type Writer interface { Read() string; Write() }
type ReadWriter interface { Reader; Writer }
Hint: Method names overlapping.
Bug: In Go 1.13 and earlier, Reader.Read and Writer.Read had to match exactly — otherwise a compile error. Go 1.14+ relaxed the rules: if the name and signature are identical, it is OK. Otherwise — error.
Fix: Make the signatures match, or rename the methods.
Bug 11 — Receiver used as value, but pointer needed¶
type DB struct{ conn *sql.DB }
func (d DB) Close() { d.conn.Close(); d.conn = nil }
func main() {
db := DB{conn: openDB()}
db.Close()
db.conn.Ping() // nil pointer panic? No!
}
Hint: Where is d.conn = nil stored?
Bug: Value receiver — d is a copy. d.conn = nil only affects the local copy. The original db.conn still exists (but it has been closed!). db.conn.Ping() returns a "use of closed connection" error.
Fix:
Bug 12 — Method expression wrong type¶
type Shape struct{}
func (s Shape) Area() float64 { return 1 }
func (s *Shape) Scale(f float64) { ... }
func main() {
f := Shape.Scale // ?
}
Hint: Pointer receiver and method expression.
Bug: Shape.Scale — the value type Shape does not have Scale. Compile error: Shape.Scale undefined.
Fix:
Bug 13 — Encapsulation external visibility¶
Hint: Exported and private.
Bug: name is lowercase — it is not visible outside the package. Compile error: u.name undefined (cannot refer to unexported field name).
Fix: Access via a method:
Or use Go-style Name() (without the Get prefix):
Bug 14 — Method receiver in goroutine — wrong reference¶
type Worker struct{ id int }
func (w Worker) Run() { fmt.Println("worker", w.id) }
func main() {
workers := []Worker{{1}, {2}, {3}}
for _, w := range workers {
go w.Run() // ?
}
time.Sleep(100*time.Millisecond)
}
Hint: Goroutine and loop variable. Check the Go version.
Bug: - Go 1.21 and earlier: w is the same variable on every iteration. The w.Run method value takes a fresh copy each time (value receiver), so this is NOT A BUG — each worker runs with its own id. But if it were a pointer receiver — it would be a bug. - Go 1.22+ — a new w per iteration. No bug at all.
Explanation: This exercise is a careful question — value vs pointer receiver and Go version both matter.
Bug 15 — Recursive method call without termination¶
Hint: No base case.
Bug: If l.next is nil — nil pointer dereference. It never terminates (or panics).
Fix:
Bug 16 — Functional options misused¶
type Server struct{ port int }
type Option func(*Server)
func WithPort(p int) Option { return func(s *Server) { s.port = p } }
func NewServer(opts ...Option) *Server {
s := &Server{port: 80}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
func main() {
s := NewServer(WithPort) // ?
}
Hint: WithPort requires an argument.
Bug: WithPort itself is func(int) Option — it takes an argument. NewServer(WithPort) — WithPort is not an Option but a function returning an Option. Compile error: cannot use WithPort as Option.
Fix:
Bug 17 — Method on value does not add up¶
type Money struct{ cents int }
func (m Money) Add(other Money) { m.cents += other.cents }
func main() {
a := Money{cents: 100}
b := Money{cents: 50}
a.Add(b)
fmt.Println(a.cents) // ?
}
Hint: Value receiver behavior.
Bug: Add has a value receiver — m.cents += other.cents modifies the local copy. a is unaffected. Output: 100.
Fix (immutable style — return a new value):
Or use a pointer receiver:
Money is a value object — typically the immutable style is preferred.
Bug 18 — Method returning its own type¶
type Builder struct{ parts []string }
func (b Builder) Add(s string) Builder {
b.parts = append(b.parts, s)
return b
}
func main() {
b := Builder{}
b.Add("a")
b.Add("b")
fmt.Println(b.parts) // ?
}
Hint: The Builder method chain isn't keeping the returned value.
Bug: Add returns a new Builder (immutable), but the result of b.Add("a") is ignored. Output: [].
Fix:
Or use a pointer receiver for a mutable builder:
Cheat Sheet¶
TYPICAL BUGS
─────────────────────────────
1. Value receiver mutates → original unchanged
2. Pointer receiver on map element → cannot take address
3. Mutex with value receiver → race condition
4. Method set: Person ↔ *Person → interface match fails
5. Nil pointer receiver → panic (defensive coding)
6. Loop variable + method value → Go 1.21 issue
7. Builder result ignored → empty result
8. Adding methods cross-package → compile error
9. Adding methods to an alias → compile error
10. Embedded conflict → ambiguous selector
GO VET HELP
─────────────────────────────
go vet ./... # catches most bugs
go run -race ./... # race conditions
staticcheck ./... # additional checks