Go switch Statement — Find the Bug¶
Instructions¶
Each exercise contains buggy Go code. Your task is to identify the bug, explain why it is wrong, and provide the correct fix. Difficulty levels: 🟢 Easy, 🟡 Medium, 🔴 Hard.
Bug 1 🟢 — Missing Default in Security Check¶
package main
import "fmt"
func checkPermission(role string) bool {
switch role {
case "admin":
return true
case "user":
return false
}
return true // intention: fallback to allow
}
func main() {
fmt.Println(checkPermission("admin")) // true
fmt.Println(checkPermission("user")) // false
fmt.Println(checkPermission("attacker")) // ???
}
What is the bug?
Hint
What does the function return when the role is not "admin" or "user"? Is that the intended behavior for an unknown role?Solution
**Bug**: The function returns `true` (allow) for any unknown role. An attacker can pass any string not in the switch and gain access. The `return true` after the switch is a security vulnerability. **Fix**: **Key lesson**: In security-sensitive code, always use `default` that denies/rejects. Never fall through to a permissive default.Bug 2 🟢 — Break Does Not Exit the Loop¶
package main
import "fmt"
func findFirst(items []string, target string) int {
for i, item := range items {
switch item {
case target:
fmt.Printf("Found at index %d\n", i)
break // intention: stop searching
}
}
return -1
}
func main() {
items := []string{"apple", "banana", "cherry", "date"}
findFirst(items, "banana")
// Expected: prints once and stops
// Actual: keeps looping
}
What is the bug?
Hint
What does `break` exit when it is inside a switch that is inside a for loop?Solution
**Bug**: `break` inside a switch exits only the switch, not the enclosing `for` loop. The loop continues checking all items even after finding the target. **Fix** (use labeled break): **Alternative fix** (use return):Bug 3 🟢 — Fallthrough is Unconditional¶
package main
import "fmt"
func describeNumber(n int) {
switch {
case n < 0:
fmt.Println("negative")
fallthrough
case n == 0:
fmt.Println("zero")
case n > 0:
fmt.Println("positive")
}
}
func main() {
describeNumber(-5)
// Expected: "negative"
// Actual: ???
}
What is the bug?
Hint
What does `fallthrough` do exactly? Does it check the next case's condition?Solution
**Bug**: `fallthrough` does NOT check the condition of the next case. It unconditionally executes the next case body. So for `n = -5`, the code prints "negative" AND "zero" even though `n == 0` is false. **Output of buggy code for -5**: **Fix** (remove fallthrough): **Key lesson**: `fallthrough` is unconditional — it always executes the next case body regardless of that case's condition.Bug 4 🟢 — fallthrough in Last Case¶
package main
import "fmt"
func process(x int) {
switch x {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("two")
fallthrough // BUG
}
}
func main() {
process(1)
}
What is the bug?
Hint
Where is `fallthrough` not allowed?Solution
**Bug**: `fallthrough` cannot appear in the last case of a switch statement. This is a **compile error**: `cannot fallthrough final case in switch`. **Fix**: **Key lesson**: `fallthrough` can only appear in a non-final case. If you want the last case to do something additional, call a function explicitly.Bug 5 🟡 — Typed Nil Does Not Match nil Case¶
package main
import "fmt"
type MyError struct{ msg string }
func (e *MyError) Error() string { return e.msg }
func process() error {
var err *MyError = nil // typed nil
return err // returns typed nil as error interface
}
func main() {
err := process()
switch err {
case nil:
fmt.Println("no error") // Expected to print this
default:
fmt.Println("error occurred:", err) // Actually prints this!
}
}
What is the bug?
Hint
An `error` interface value is nil only when BOTH its type AND value pointers are nil. What happens when you return a typed nil as an interface?Solution
**Bug**: When `*MyError(nil)` is returned as `error`, the resulting interface has a non-nil type pointer (`*MyError`) and a nil value pointer. This interface is NOT nil even though the underlying value is nil. So `err == nil` is false. **Fix 1** — Return untyped nil:func process() error {
// Check the typed nil before returning
var err *MyError = nil
if err == nil {
return nil // return untyped nil
}
return err
}
err := process()
if err == nil {
fmt.Println("no error")
} else {
fmt.Println("error:", err)
}
// This still won't work for the typed nil case
// Correct check:
switch e := err.(type) {
case *MyError:
if e == nil {
fmt.Println("no error (typed nil)")
} else {
fmt.Println("MyError:", e.msg)
}
case nil:
fmt.Println("no error")
}
Bug 6 🟡 — Multiple Interface Types per Case Loses Type Info¶
package main
import "fmt"
type Cat struct{ name string }
type Dog struct{ name string }
func (c Cat) Speak() string { return "meow" }
func (d Dog) Speak() string { return "woof" }
func greet(animal interface{}) {
switch v := animal.(type) {
case Cat, Dog: // Multiple types in one case
fmt.Printf("Hello %s, you say %s\n", v.name, v.Speak())
// BUG: v.name and v.Speak() won't work here
}
}
func main() {
greet(Cat{name: "Whiskers"})
greet(Dog{name: "Rex"})
}
What is the bug?
Hint
When multiple types are listed in a single type switch case, what type does `v` have?Solution
**Bug**: When multiple types appear in one case (`case Cat, Dog:`), the variable `v` has type `interface{}` — NOT `Cat` or `Dog`. You cannot call `v.name` (unexported via interface) or `v.Speak()` (not in the interface) on an `interface{}` value. This is a **compile error**. **Fix** — Separate the cases:type Animal interface {
Speak() string
Name() string
}
type Cat struct{ name string }
type Dog struct{ name string }
func (c Cat) Speak() string { return "meow" }
func (d Dog) Speak() string { return "woof" }
func (c Cat) Name() string { return c.name }
func (d Dog) Name() string { return d.name }
func greet(animal interface{}) {
switch v := animal.(type) {
case Cat:
fmt.Printf("Hello %s, you say %s\n", v.name, v.Speak())
case Dog:
fmt.Printf("Hello %s, you say %s\n", v.name, v.Speak())
default:
fmt.Printf("Unknown animal: %T\n", v)
}
}
Bug 7 🟡 — Switch Expression Evaluated With Side Effect¶
package main
import "fmt"
var callCount int
func getValue() int {
callCount++
return 42
}
func main() {
switch getValue() {
case 42:
fmt.Println("got 42")
// Thinking: re-evaluate getValue() for each case check
case 0:
fmt.Println("got 0")
}
fmt.Printf("getValue called %d time(s)\n", callCount)
// Expected by confused developer: called 2 times
// Actual: called 1 time
}
What is the bug?
Hint
How many times is the switch expression evaluated? Is this a bug in understanding or in the code?Solution
**Bug**: This is a conceptual misunderstanding. The switch expression is evaluated EXACTLY ONCE, not once per case. So `getValue()` is called only 1 time. The developer may have incorrectly assumed it was called for each case. **This is actually correct behavior** — but if a developer writes code EXPECTING multiple evaluations, they have a design bug. **Example of the real bug** — modifying state in the switch expression: **Key lesson**: The switch expression is evaluated once. Do not use expressions with side effects in switch.Bug 8 🟡 — Using switch in goroutine Without Synchronization¶
package main
import (
"fmt"
"sync"
)
type State int
const (
StateIdle State = iota
StateRunning
StateStopped
)
type Machine struct {
state State
}
func (m *Machine) Process(cmd string) {
// BUG: reading m.state without lock
switch m.state {
case StateIdle:
if cmd == "start" {
m.state = StateRunning
fmt.Println("started")
}
case StateRunning:
if cmd == "stop" {
m.state = StateStopped
fmt.Println("stopped")
}
case StateStopped:
fmt.Println("already stopped")
}
}
func main() {
m := &Machine{state: StateIdle}
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
m.Process("start")
}()
}
wg.Wait()
}
What is the bug?
Hint
What happens when multiple goroutines read and write `m.state` concurrently without synchronization?Solution
**Bug**: Data race. Multiple goroutines concurrently read `m.state` (in the switch) and write it (`m.state = StateRunning`). This is undefined behavior in Go's memory model. Detect with: `go run -race main.go` **Fix** — Use a mutex:type Machine struct {
mu sync.Mutex
state State
}
func (m *Machine) Process(cmd string) {
m.mu.Lock()
defer m.mu.Unlock()
switch m.state {
case StateIdle:
if cmd == "start" {
m.state = StateRunning
fmt.Println("started")
}
case StateRunning:
if cmd == "stop" {
m.state = StateStopped
fmt.Println("stopped")
}
case StateStopped:
fmt.Println("already stopped")
}
}
Bug 9 🔴 — Fallthrough in Type Switch¶
package main
import "fmt"
func process(v interface{}) {
switch t := v.(type) {
case int:
fmt.Println("int:", t)
fallthrough // attempt to share string processing
case string:
fmt.Println("string:", t)
default:
fmt.Printf("other: %T\n", t)
}
}
func main() {
process(42)
process("hello")
}
What is the bug?
Hint
Is `fallthrough` allowed in type switches?Solution
**Bug**: `fallthrough` is NOT allowed in type switch statements. This is a **compile error**: `cannot fallthrough in type switch`. The reason: in a type switch, `t` is bound to a different concrete type in each case. Using `fallthrough` would require `t` to be both `int` and `string` simultaneously — impossible. **Fix** — Extract shared logic to a function: **Key lesson**: Shared logic between type switch cases must be expressed via function calls, not `fallthrough`.Bug 10 🔴 — Incorrect State Transition Order¶
package main
import "fmt"
type Phase int
const (
PhaseInit Phase = iota
PhaseRunning
PhaseDone
PhaseError
)
func (p Phase) String() string {
switch p {
case PhaseInit: return "Init"
case PhaseRunning: return "Running"
case PhaseDone: return "Done"
case PhaseError: return "Error"
default: return "Unknown"
}
}
func advance(p Phase) Phase {
switch p {
case PhaseInit:
return PhaseRunning
case PhaseRunning:
return PhaseDone
case PhaseDone:
return PhaseInit // BUG: resets to Init instead of terminal
default:
return PhaseError
}
}
func runPipeline() {
phase := PhaseInit
for phase != PhaseDone {
fmt.Printf("Phase: %s\n", phase)
phase = advance(phase)
if phase == PhaseError {
fmt.Println("Error!")
return
}
}
fmt.Printf("Phase: %s\n", phase)
fmt.Println("Pipeline complete!")
}
func main() {
runPipeline()
}
What is the bug?
Hint
What happens when `advance(PhaseDone)` is called? Does the loop termination condition ever become true?Solution
**Bug**: `advance(PhaseDone)` returns `PhaseInit` instead of `PhaseDone` (terminal). The loop condition is `phase != PhaseDone`, so when we reach Done, we advance it back to Init, and the pipeline loops forever (or until PhaseError which never happens here). **Expected behavior**: `PhaseDone` is a terminal state — it should not advance. **Fix**:func advance(p Phase) Phase {
switch p {
case PhaseInit:
return PhaseRunning
case PhaseRunning:
return PhaseDone
case PhaseDone:
return PhaseDone // terminal — stay in Done
default:
return PhaseError
}
}
func advance(p Phase) (Phase, error) {
switch p {
case PhaseInit:
return PhaseRunning, nil
case PhaseRunning:
return PhaseDone, nil
case PhaseDone:
return PhaseDone, fmt.Errorf("advance called on terminal state Done")
default:
return PhaseError, fmt.Errorf("advance called on unknown phase: %v", p)
}
}
Bug 11 🔴 — Switch on Interface{} vs concrete type¶
package main
import "fmt"
func classify(v interface{}) string {
switch v {
case 0:
return "zero int"
case "":
return "empty string"
case false:
return "false bool"
default:
return fmt.Sprintf("other: %v (%T)", v, v)
}
}
func main() {
fmt.Println(classify(0))
fmt.Println(classify(int64(0))) // Does this match case 0?
fmt.Println(classify(0.0)) // Does this match case 0?
fmt.Println(classify(""))
fmt.Println(classify(false))
}
What is the bug?
Hint
When switching on an `interface{}`, how does Go compare case values? Does `0` (int) equal `int64(0)` or `0.0` (float64)?Solution
**Bug**: When switching on `interface{}`, Go uses interface equality. Two interface values are equal only if they have the same dynamic type AND equal values. So: - `interface{}(0)` (type=int) != `interface{}(int64(0))` (type=int64) — different types! - `interface{}(0)` (type=int) != `interface{}(0.0)` (type=float64) — different types! **Output of buggy code**:zero int
other: 0 (int64) ← did NOT match case 0
other: 0 (float64) ← did NOT match case 0
empty string
false bool
func classify(v interface{}) string {
switch val := v.(type) {
case int:
if val == 0 { return "zero int" }
return fmt.Sprintf("int: %d", val)
case int64:
if val == 0 { return "zero int64" }
return fmt.Sprintf("int64: %d", val)
case float64:
if val == 0.0 { return "zero float64" }
return fmt.Sprintf("float64: %f", val)
case string:
if val == "" { return "empty string" }
return fmt.Sprintf("string: %q", val)
case bool:
if !val { return "false bool" }
return "true bool"
default:
return fmt.Sprintf("other: %v (%T)", val, val)
}
}
Bug 12 🔴 — Init Statement Variable Shadow¶
package main
import "fmt"
func getStatus() string { return "active" }
func main() {
status := "inactive"
fmt.Println("Before:", status)
switch status := getStatus(); status {
case "active":
fmt.Println("Switch: active")
// Intend to use outer status here
fmt.Println("Outer status:", status) // BUG: prints inner status
case "inactive":
fmt.Println("Switch: inactive")
}
fmt.Println("After:", status) // Is this "inactive" or "active"?
}
What is the bug?
Hint
The switch init statement declares a new `status` variable. Does it shadow the outer `status`?Solution
**Bug**: The init statement `status := getStatus()` declares a NEW variable `status` scoped to the switch block. It shadows the outer `status`. Inside the switch cases, `status` refers to the inner variable ("active"), not the outer one ("inactive"). **Output**:Before: inactive
Switch: active
Outer status: active ← shows inner "active", not outer "inactive"
After: inactive ← outer status is unchanged