Go switch — Middle Level¶
Table of Contents¶
- Why switch Exists — Design Rationale
- Go switch vs C/Java switch — Key Differences
- switch Internals: Jump Tables and Binary Search
- Expressionless switch — When and Why
- Type switch — Deep Dive
- Type switch with Multiple Interface Types
- fallthrough — Semantics and Use Cases
- switch in Error Handling
- switch with Interface Values
- Switch and the Comparable Constraint
- Exhaustiveness Checking
- switch in State Machines
- switch vs Map Dispatch — Trade-offs
- switch vs if-else Chain — Performance
- Labeled break in Loops with switch
- switch with Goroutines and Select
- Testing switch Statements
- Anti-Patterns in Go switch
- Refactoring: if-else to switch
- switch and Cyclomatic Complexity
- switch in Code Generation
- Evolution of switch in Go Versions
- Alternative Approaches: dispatch tables, interfaces
- Language Comparison: Go vs Rust match
- switch in Standard Library
- Debugging switch Logic
- switch and Zero Values
- switch with Error Types
- Performance: Switch vs Map for Different Sizes
- Debugging Guide and Common Pitfalls
1. Why switch Exists — Design Rationale¶
switch is fundamentally about dispatching on a value. Its existence simplifies code where you have multiple discrete outcomes:
// Without switch: verbose if-else chain
func getHandler(path string) http.HandlerFunc {
if path == "/" { return homeHandler }
if path == "/about" { return aboutHandler }
if path == "/contact" { return contactHandler }
return notFoundHandler
}
// With switch: cleaner dispatch
func getHandler(path string) http.HandlerFunc {
switch path {
case "/": return homeHandler
case "/about": return aboutHandler
case "/contact": return contactHandler
default: return notFoundHandler
}
}
Go's switch design decisions: - No implicit fallthrough: Prevents accidental bugs that plague C code - Multiple values per case: case "a", "b": eliminates duplication - Expressionless switch: Replaces if-else chains cleanly - Type switch: Replaces reflect.TypeOf() comparisons
2. Go switch vs C/Java switch — Key Differences¶
// Go switch — key differences:
// 1. No implicit fallthrough
switch x {
case 1:
fmt.Println("one")
// Automatically exits switch — no "break" needed
case 2:
fmt.Println("two")
}
// 2. Explicit fallthrough (unconditional!)
switch x {
case 1:
fmt.Println("one")
fallthrough // Always falls to case 2, regardless of x
case 2:
fmt.Println("two")
}
// 3. Multiple values per case
switch x {
case 1, 2, 3:
fmt.Println("small")
}
// C equivalent: case 1: case 2: case 3: (separate lines!)
// 4. Expression-less switch
switch {
case x > 0:
fmt.Println("positive")
}
// C: switch(1) { case (x > 0): } — not idiomatic in C
// 5. Type switch (no C equivalent)
switch v := i.(type) {
case int: fmt.Println("int", v)
}
| Feature | Go | C | Java |
|---|---|---|---|
| Implicit fallthrough | No | Yes | No (enhanced switch) |
| Explicit fallthrough | fallthrough | (default) | No |
| Multiple values/case | case 1,2,3: | Separate cases | case 1,2,3: (switch expr) |
| Expression-less switch | Yes | No | No |
| Type switch | Yes | No | instanceof chain |
| break required | No | Yes | No (enhanced switch) |
| Return from switch | Yes | Yes | Yes |
3. switch Internals: Jump Tables and Binary Search¶
For small numbers of integer cases, the compiler generates a jump table (O(1)):
// This likely compiles to a jump table (O(1))
switch n {
case 0: return "zero"
case 1: return "one"
case 2: return "two"
case 3: return "three"
case 4: return "four"
}
Assembly (simplified):
CMPQ n, $4
JA default ; if n > 4, go to default
JMPQ *table(n*8) ; jump table lookup
table:
.quad case0
.quad case1
.quad case2
.quad case3
.quad case4
For sparse integer values or large ranges, the compiler may use binary search (O(log n)):
// Sparse values — binary search (not jump table)
switch n {
case 1: return "a"
case 10: return "b"
case 100: return "c"
case 1000: return "d"
}
For string switches, the compiler often generates: 1. Compare lengths first (cheap) 2. Hash comparison or sequential comparison
View with:
4. Expressionless switch — When and Why¶
Expressionless switch is switch true:
// These are identical:
switch true {
case x > 0:
fmt.Println("positive")
}
switch {
case x > 0:
fmt.Println("positive")
}
When to use expressionless switch: 1. Range conditions on different variables 2. Complex boolean expressions per case 3. When cases test different things
func describeNumber(n float64) string {
switch {
case math.IsNaN(n):
return "NaN"
case math.IsInf(n, 1):
return "+Infinity"
case math.IsInf(n, -1):
return "-Infinity"
case n < 0:
return "negative"
case n == 0:
return "zero"
case n < 1:
return "fractional positive"
default:
return "positive"
}
}
Vs if-else: For 4+ unrelated boolean conditions, switch is cleaner. For 2-3 conditions, if-else is fine.
5. Type switch — Deep Dive¶
Type switch is powered by Go's runtime type information (RTTI):
package main
import "fmt"
// The type assertion x.(type) is only valid in a switch statement
// var v interface{} = 42
// t := v.(type) // COMPILE ERROR: use of .(type) outside type switch
func processAny(v interface{}) {
switch x := v.(type) {
case nil:
fmt.Println("nil")
case int, int8, int16, int32, int64:
fmt.Printf("some integer: %v (type %T)\n", x, x)
// NOTE: x is still interface{} here when multiple types!
case uint, uint8, uint16, uint32, uint64:
fmt.Printf("unsigned integer: %v\n", x)
case float32:
fmt.Printf("float32: %f\n", x)
case float64:
fmt.Printf("float64: %f\n", x)
case string:
fmt.Printf("string of length %d: %q\n", len(x), x)
case []byte:
fmt.Printf("byte slice of length %d\n", len(x))
case error:
fmt.Printf("error: %v\n", x)
default:
fmt.Printf("unhandled type: %T\n", x)
}
}
func main() {
processAny(42)
processAny("hello")
processAny(3.14)
processAny([]byte{1, 2, 3})
processAny(fmt.Errorf("test"))
processAny(nil)
}
Key: When a case lists multiple types, x retains the interface type (not the concrete type).
6. Type switch with Multiple Interface Types¶
package main
import (
"fmt"
"io"
"os"
)
type Closer interface{ Close() error }
type Flusher interface{ Flush() error }
type ReadWriter interface {
io.Reader
io.Writer
}
func describe(v interface{}) string {
switch v := v.(type) {
case ReadWriter:
return fmt.Sprintf("ReadWriter: %T", v)
case io.Writer:
return fmt.Sprintf("Writer only: %T", v)
case io.Reader:
return fmt.Sprintf("Reader only: %T", v)
case Closer:
return fmt.Sprintf("Closer: %T", v)
default:
return fmt.Sprintf("other: %T", v)
}
}
func main() {
describe(os.Stdout) // *os.File implements ReadWriter
describe(os.Stdin) // *os.File implements ReadWriter
_ = v // silence unused
}
var v = describe
Important: Interface cases in type switch check if the value implements the interface. More specific interfaces should come first.
7. fallthrough — Semantics and Use Cases¶
fallthrough in Go is unconditional — it does NOT check the next case's condition:
package main
import "fmt"
func main() {
x := 1
switch x {
case 1:
fmt.Println("one")
fallthrough // Always executes case 2 body, regardless of condition
case 2:
fmt.Println("two or fell through from one")
}
// Output: "one" then "two or fell through from one"
}
// Legitimate use: version compatibility
func handleVersion(version int) {
switch {
case version >= 3:
fmt.Println("v3 features")
fallthrough
case version >= 2:
fmt.Println("v2 features")
fallthrough
case version >= 1:
fmt.Println("v1 features")
}
}
// Legitimate use: accumulating behaviors
func describeN(n int) {
s := ""
switch {
case n >= 3:
s += "[3+]"
fallthrough
case n >= 2:
s += "[2+]"
fallthrough
case n >= 1:
s += "[1+]"
}
fmt.Println(n, "->", s)
}
When NOT to use fallthrough: Most of the time. It reduces readability and is a code smell in most Go code.
8. switch in Error Handling¶
package main
import (
"errors"
"fmt"
"net"
"syscall"
)
type NotFoundError struct{ ID string }
func (e *NotFoundError) Error() string { return "not found: " + e.ID }
type PermissionError struct{ Action string }
func (e *PermissionError) Error() string { return "permission denied: " + e.Action }
type RateLimitError struct{ RetryAfter int }
func (e *RateLimitError) Error() string { return fmt.Sprintf("rate limited, retry after %ds", e.RetryAfter) }
func handleError(err error) {
if err == nil {
return
}
// Type switch for error handling
switch e := err.(type) {
case *NotFoundError:
fmt.Printf("Resource %s not found — check the ID\n", e.ID)
case *PermissionError:
fmt.Printf("No permission to %s — check your role\n", e.Action)
case *RateLimitError:
fmt.Printf("Rate limited — try again in %d seconds\n", e.RetryAfter)
case *net.OpError:
fmt.Printf("Network error: op=%s, net=%s\n", e.Op, e.Net)
default:
// Check wrapped errors with errors.Is
if errors.Is(err, syscall.ECONNREFUSED) {
fmt.Println("Connection refused — is the server running?")
} else {
fmt.Printf("Unknown error: %T: %v\n", err, err)
}
}
}
func main() {
handleError(&NotFoundError{"user-42"})
handleError(&PermissionError{"delete"})
handleError(&RateLimitError{30})
handleError(fmt.Errorf("some other error"))
}
9. switch with Interface Values¶
package main
import "fmt"
type Animal interface {
Speak() string
Name() string
}
type Dog struct{ name string }
func (d Dog) Speak() string { return "Woof!" }
func (d Dog) Name() string { return d.name }
type Cat struct{ name string }
func (c Cat) Speak() string { return "Meow!" }
func (c Cat) Name() string { return c.name }
type Bird struct{ name string }
func (b Bird) Speak() string { return "Tweet!" }
func (b Bird) Name() string { return b.name }
func describeAnimal(a Animal) {
// Type switch gives you access to concrete type's methods
switch pet := a.(type) {
case Dog:
fmt.Printf("Dog %s says: %s (loyal!)\n", pet.Name(), pet.Speak())
case Cat:
fmt.Printf("Cat %s says: %s (independent!)\n", pet.Name(), pet.Speak())
case Bird:
fmt.Printf("Bird %s says: %s (free!)\n", pet.Name(), pet.Speak())
default:
fmt.Printf("Unknown animal: %T says %s\n", a, a.Speak())
}
}
func main() {
animals := []Animal{
Dog{"Rex"},
Cat{"Whiskers"},
Bird{"Tweety"},
}
for _, a := range animals {
describeAnimal(a)
}
}
10. Switch and the Comparable Constraint¶
Go switch can be used with any comparable type. Non-comparable types (slices, maps, functions) cannot be used as case values:
package main
import "fmt"
type Point struct{ X, Y int }
func main() {
// Comparable types work
p := Point{1, 2}
switch p {
case Point{0, 0}:
fmt.Println("origin")
case Point{1, 2}:
fmt.Println("target") // This matches
}
// Arrays are comparable
arr := [3]int{1, 2, 3}
switch arr {
case [3]int{1, 2, 3}:
fmt.Println("our array")
}
// Slices, maps, functions are NOT comparable
// switch []int{1,2,3} { case []int{1,2,3}: } // COMPILE ERROR
// Workaround: use expressionless switch
s := []int{1, 2, 3}
switch {
case len(s) == 3 && s[0] == 1:
fmt.Println("might be our slice")
}
}
11. Exhaustiveness Checking¶
Go's switch doesn't require exhaustive case coverage (unlike Rust's match). This can lead to unhandled cases:
package main
import "fmt"
type Status int
const (
Pending Status = iota
Active
Suspended
Deleted
)
// BUG: Deleted status is not handled!
func processStatus(s Status) string {
switch s {
case Pending:
return "pending"
case Active:
return "active"
case Suspended:
return "suspended"
// Deleted is missing!
}
return "" // Silent default
}
func main() {
fmt.Println(processStatus(Deleted)) // Returns "" silently
}
Tools for exhaustiveness:
# Install exhaustive linter
go install github.com/nishanths/exhaustive/cmd/exhaustive@latest
exhaustive ./...
// Using default to catch missing cases (but loses type safety)
switch s {
case Pending: return "pending"
case Active: return "active"
case Suspended: return "suspended"
default:
panic(fmt.Sprintf("unhandled status: %d", s)) // Fail-fast
}
12. switch in State Machines¶
Switch is the natural fit for FSM implementations:
package main
import "fmt"
type State int
const (
StateIdle State = iota
StateConnecting
StateConnected
StateDisconnecting
)
type Event int
const (
EventConnect Event = iota
EventConnected
EventDisconnect
EventDisconnected
EventError
)
func transition(current State, event Event) (State, error) {
switch current {
case StateIdle:
switch event {
case EventConnect:
return StateConnecting, nil
default:
return current, fmt.Errorf("invalid event %d in idle state", event)
}
case StateConnecting:
switch event {
case EventConnected:
return StateConnected, nil
case EventError:
return StateIdle, nil
default:
return current, fmt.Errorf("invalid event %d in connecting state", event)
}
case StateConnected:
switch event {
case EventDisconnect:
return StateDisconnecting, nil
case EventError:
return StateIdle, nil
default:
return current, fmt.Errorf("invalid event %d in connected state", event)
}
case StateDisconnecting:
switch event {
case EventDisconnected:
return StateIdle, nil
default:
return current, fmt.Errorf("invalid event %d in disconnecting state", event)
}
}
return current, fmt.Errorf("unknown state %d", current)
}
func main() {
state := StateIdle
events := []Event{EventConnect, EventConnected, EventDisconnect, EventDisconnected}
stateNames := map[State]string{
StateIdle: "Idle", StateConnecting: "Connecting",
StateConnected: "Connected", StateDisconnecting: "Disconnecting",
}
for _, e := range events {
next, err := transition(state, e)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("%s -> %s\n", stateNames[state], stateNames[next])
state = next
}
}
}
13. switch vs Map Dispatch — Trade-offs¶
package main
import (
"fmt"
"testing"
)
// switch approach: compile-time checked, no closures
func processSwitch(cmd string) string {
switch cmd {
case "start": return "starting"
case "stop": return "stopping"
case "restart": return "restarting"
case "status": return "checking status"
default: return "unknown"
}
}
// map approach: runtime extensible, closures
var handlers = map[string]func() string{
"start": func() string { return "starting" },
"stop": func() string { return "stopping" },
"restart": func() string { return "restarting" },
"status": func() string { return "checking status" },
}
func processMap(cmd string) string {
if h, ok := handlers[cmd]; ok {
return h()
}
return "unknown"
}
func BenchmarkSwitch(b *testing.B) {
for i := 0; i < b.N; i++ {
processSwitch("restart")
}
}
func BenchmarkMap(b *testing.B) {
for i := 0; i < b.N; i++ {
processMap("restart")
}
}
Trade-offs:
| Aspect | switch | map dispatch |
|---|---|---|
| Performance | Fastest (jump table) | Fast (hash lookup) |
| Extensibility | Requires code change | Add to map at runtime |
| Type safety | Compile-time | Runtime (interface) |
| Closures/state | No | Yes |
| Testing | Easy | Easy |
| Number of cases | Works well < 20 | Better for 50+ |
14. switch vs if-else Chain — Performance¶
package main
import "testing"
var result string
func classify_switch(n int) string {
switch {
case n < -100: return "very negative"
case n < 0: return "negative"
case n == 0: return "zero"
case n < 100: return "positive"
default: return "very positive"
}
}
func classify_ifelsechain(n int) string {
if n < -100 {
return "very negative"
} else if n < 0 {
return "negative"
} else if n == 0 {
return "zero"
} else if n < 100 {
return "positive"
} else {
return "very positive"
}
}
func BenchmarkSwitch(b *testing.B) {
for i := 0; i < b.N; i++ {
result = classify_switch(i % 300 - 150)
}
}
func BenchmarkIfElse(b *testing.B) {
for i := 0; i < b.N; i++ {
result = classify_ifelsechain(i % 300 - 150)
}
}
// Result: essentially identical — same machine code generated
Conclusion: For expressionless switch vs if-else, performance is equivalent. The difference comes from integer switch with jump tables.
15. Labeled break in Loops with switch¶
package main
import "fmt"
func searchMatrix(matrix [][]int, target int) (int, int) {
outer:
for i, row := range matrix {
for j, val := range row {
if val == target {
fmt.Printf("Found %d at (%d,%d)\n", target, i, j)
break outer // Exits both loops!
}
}
// Without label: switch break only exits switch, not the for loop
switch {
case i > 10:
break outer // Exits outer for loop
}
}
return -1, -1
}
func processCommands(commands []string) {
loop:
for _, cmd := range commands {
switch cmd {
case "quit":
fmt.Println("Quitting")
break loop // Exits the for loop (not just switch)
case "skip":
continue loop // continue works with labels too
default:
fmt.Println("Processing:", cmd)
}
}
}
func main() {
matrix := [][]int{{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}
searchMatrix(matrix, 5)
processCommands([]string{"start", "skip", "work", "quit", "after"})
}
16. switch with Goroutines and Select¶
package main
import (
"fmt"
"time"
)
func processor(jobs <-chan string, results chan<- string, done <-chan struct{}) {
for {
select {
case job, ok := <-jobs:
if !ok {
return
}
// Inner switch to process job type
switch job {
case "heavy":
time.Sleep(10 * time.Millisecond)
results <- "heavy done"
case "light":
results <- "light done"
default:
results <- "unknown: " + job
}
case <-done:
fmt.Println("Worker shutting down")
return
}
}
}
func main() {
jobs := make(chan string, 5)
results := make(chan string, 5)
done := make(chan struct{})
go processor(jobs, results, done)
jobs <- "heavy"
jobs <- "light"
jobs <- "unknown"
close(jobs)
for i := 0; i < 3; i++ {
fmt.Println(<-results)
}
}
Note: select itself is a special form of switch for channels.
17. Testing switch Statements¶
package main
import (
"testing"
)
func httpStatusCategory(code int) string {
switch {
case code >= 500:
return "server_error"
case code >= 400:
return "client_error"
case code >= 300:
return "redirect"
case code >= 200:
return "success"
case code >= 100:
return "informational"
default:
return "unknown"
}
}
func TestHttpStatusCategory(t *testing.T) {
tests := []struct {
code int
want string
}{
// Test boundaries for each case
{200, "success"},
{299, "success"},
{300, "redirect"},
{399, "redirect"},
{400, "client_error"},
{404, "client_error"},
{499, "client_error"},
{500, "server_error"},
{503, "server_error"},
{100, "informational"},
{99, "unknown"},
{0, "unknown"},
}
for _, tt := range tests {
t.Run(fmt.Sprintf("code=%d", tt.code), func(t *testing.T) {
if got := httpStatusCategory(tt.code); got != tt.want {
t.Errorf("httpStatusCategory(%d) = %q; want %q",
tt.code, got, tt.want)
}
})
}
}
Coverage requirement: At least one test per case, including the boundary values.
18. Anti-Patterns in Go switch¶
package main
import "fmt"
// Anti-pattern 1: Using switch where if is cleaner (2 cases)
func isPositive_bad(n int) string {
switch {
case n > 0:
return "positive"
default:
return "non-positive"
}
}
// Better: just use if-else for 2 cases
// Anti-pattern 2: Using fallthrough unnecessarily
func monthDays_bad(month int) int {
days := 0
switch month {
case 1:
days = 31
fallthrough
case 2:
days = 28 // BUG: overwrites 31!
}
return days
}
// Correct:
func monthDays_good(month int) int {
switch month {
case 2:
return 28
case 4, 6, 9, 11:
return 30
default:
return 31
}
}
// Anti-pattern 3: Checking same condition twice
func check_bad(x int) {
switch x > 0 {
case true:
fmt.Println("positive")
case false:
fmt.Println("non-positive")
}
}
// Better: if x > 0 { ... } else { ... }
// Anti-pattern 4: Using type switch when interface is better
func process_bad(v interface{}) {
switch v.(type) {
case *Dog:
v.(*Dog).Bark() // redundant type assertion
}
}
// Better: use the typed variable from switch
func process_good(v interface{}) {
switch dog := v.(type) {
case *Dog:
dog.Bark()
}
}
type Dog struct{}
func (d *Dog) Bark() { fmt.Println("Woof") }
19. Refactoring: if-else to switch¶
package main
import "fmt"
// BEFORE: if-else chain (hard to read)
func handleHTTPMethod_before(method string) {
if method == "GET" {
fmt.Println("Fetching resource")
} else if method == "POST" {
fmt.Println("Creating resource")
} else if method == "PUT" {
fmt.Println("Updating resource")
} else if method == "PATCH" {
fmt.Println("Partially updating")
} else if method == "DELETE" {
fmt.Println("Deleting resource")
} else if method == "HEAD" || method == "OPTIONS" {
fmt.Println("Metadata request")
} else {
fmt.Println("Method not allowed:", method)
}
}
// AFTER: switch (clean, scannable)
func handleHTTPMethod_after(method string) {
switch method {
case "GET":
fmt.Println("Fetching resource")
case "POST":
fmt.Println("Creating resource")
case "PUT":
fmt.Println("Updating resource")
case "PATCH":
fmt.Println("Partially updating")
case "DELETE":
fmt.Println("Deleting resource")
case "HEAD", "OPTIONS":
fmt.Println("Metadata request")
default:
fmt.Println("Method not allowed:", method)
}
}
func main() {
methods := []string{"GET", "POST", "DELETE", "TRACE"}
for _, m := range methods {
handleHTTPMethod_after(m)
}
}
When to refactor if-else to switch: - 4+ branches comparing the same variable - The cases are discrete values (not ranges) - You want cleaner visual structure
20. switch and Cyclomatic Complexity¶
Each case in a switch adds 1 to cyclomatic complexity (same as each branch in if-else):
// Cyclomatic complexity: 1 (base) + 4 (cases) + 1 (default) = 6
func getDiscount(tier string) float64 { // CC = 1
switch tier { // +0 (switch itself)
case "bronze": return 0.05 // +1
case "silver": return 0.10 // +1
case "gold": return 0.15 // +1
case "platinum": return 0.20 // +1
default: return 0.0 // +1
}
} // Total: 6
Reducing complexity with a map (reduces to 2):
var discounts = map[string]float64{
"bronze": 0.05, "silver": 0.10,
"gold": 0.15, "platinum": 0.20,
}
func getDiscount(tier string) float64 { // CC = 1
d, ok := discounts[tier] // +0 (no branch in code)
if !ok { // +1
return 0.0
}
return d
} // Total: 2
21. switch in Code Generation¶
package main
import (
"fmt"
"strings"
)
// Generating switch statements programmatically
type SwitchCase struct {
Values []string
Body string
}
func generateSwitch(varName string, cases []SwitchCase, defaultBody string) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("switch %s {\n", varName))
for _, c := range cases {
quoted := make([]string, len(c.Values))
for i, v := range c.Values {
quoted[i] = fmt.Sprintf("%q", v)
}
sb.WriteString(fmt.Sprintf("case %s:\n %s\n",
strings.Join(quoted, ", "), c.Body))
}
if defaultBody != "" {
sb.WriteString("default:\n " + defaultBody + "\n")
}
sb.WriteString("}")
return sb.String()
}
func main() {
code := generateSwitch("status", []SwitchCase{
{[]string{"pending"}, `return "processing"`},
{[]string{"active", "running"}, `return "ok"`},
{[]string{"failed", "error"}, `return "failed"`},
}, `return "unknown"`)
fmt.Println(code)
}
22. Evolution of switch in Go Versions¶
Go's switch has been stable since 1.0. Notable related changes:
- Go 1.18 (2022): Generic functions can use switch on type parameters
- Go 1.21 (2023): min/max/clear builtins (reduce need for switch)
- No changes to switch syntax itself
// Go 1.18+: switch in generic functions
func processNumber[T int | float64](v T) string {
// Can't do type switch on T, but can do value switch
switch any(v).(type) {
case int:
return fmt.Sprintf("int: %v", v)
case float64:
return fmt.Sprintf("float64: %v", v)
}
return "unknown"
}
23. Alternative Approaches: dispatch tables, interfaces¶
package main
import "fmt"
// Approach 1: switch (good for 3-10 cases)
func processSwitch(action string) {
switch action {
case "start": fmt.Println("starting")
case "stop": fmt.Println("stopping")
default: fmt.Println("unknown")
}
}
// Approach 2: Map of functions (good for 10+ extensible cases)
var processors = map[string]func(){
"start": func() { fmt.Println("starting") },
"stop": func() { fmt.Println("stopping") },
}
func processMap(action string) {
if fn, ok := processors[action]; ok {
fn()
} else {
fmt.Println("unknown")
}
}
// Approach 3: Interface dispatch (best for complex behaviors)
type Action interface{ Execute() }
type StartAction struct{}
func (s StartAction) Execute() { fmt.Println("starting") }
type StopAction struct{}
func (s StopAction) Execute() { fmt.Println("stopping") }
func processInterface(a Action) {
a.Execute() // No switch needed!
}
24. Language Comparison: Go vs Rust match¶
// GO switch
switch status {
case 200, 201:
fmt.Println("success")
case 404:
fmt.Println("not found")
default:
fmt.Println("other")
}
// RUST match (for comparison — not Go code)
// match status {
// 200 | 201 => println!("success"),
// 404 => println!("not found"),
// _ => println!("other"),
// }
Key differences: | Feature | Go switch | Rust match | |---------|-----------|------------| | Exhaustiveness | Not required | Required | | Pattern matching | Limited | Full | | Guards | Via expressionless switch | if guards in patterns | | Binding | switch v := x.(type) | v @ pattern | | Return value | No (statement) | Yes (expression) | | Tuple matching | No | Yes | | Range patterns | Via expressionless | 1..=10 |
25. switch in Standard Library¶
// From encoding/json (simplified)
func (d *decodeState) value(v reflect.Value) {
switch d.opcode {
case scanBeginArray:
d.array(v)
case scanBeginObject:
d.object(v)
case scanBeginLiteral:
d.literalStore(d.literalInterface(), v, false)
}
}
// From net/http (simplified)
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
switch {
case r.Method != "CONNECT":
// Handle redirect
}
}
// From os package
func (f *File) Write(b []byte) (n int, err error) {
if err := f.checkValid("write"); err != nil {
return 0, err
}
n, e := f.write(b)
if n < 0 {
n = 0
}
if n != len(b) {
err = io.ErrShortWrite
}
// ...
}
26. Debugging switch Logic¶
package main
import "fmt"
// Add tracing to understand which case fires
func tracedSwitch(x int) string {
var result string
switch {
case x < 0:
result = "negative"
fmt.Printf(" [trace] case: x < 0 (x=%d)\n", x)
case x == 0:
result = "zero"
fmt.Printf(" [trace] case: x == 0\n")
case x < 100:
result = "small"
fmt.Printf(" [trace] case: 0 < x < 100 (x=%d)\n", x)
default:
result = "large"
fmt.Printf(" [trace] case: default (x=%d)\n", x)
}
return result
}
// Debugging type switch
func debugTypeSwitch(v interface{}) {
fmt.Printf("Input: %v (runtime type: %T)\n", v, v)
switch x := v.(type) {
case int:
fmt.Printf(" -> int case: %d\n", x)
case string:
fmt.Printf(" -> string case: %q\n", x)
default:
fmt.Printf(" -> default case: %T = %v\n", x, x)
}
}
func main() {
for _, n := range []int{-5, 0, 50, 200} {
fmt.Printf("Input %d -> %q\n", n, tracedSwitch(n))
}
}
27. switch and Zero Values¶
package main
import "fmt"
type Priority int
const (
Low Priority = 1
Medium Priority = 2
High Priority = 3
)
// BUG: Priority zero value (0) is unhandled!
func describeTaskBad(p Priority) string {
switch p {
case Low: return "low priority"
case Medium: return "medium priority"
case High: return "high priority"
}
return "" // Zero value returns ""
}
// GOOD: Handle zero value explicitly
func describeTask(p Priority) string {
switch p {
case 0: return "unset/default" // explicit zero value handling
case Low: return "low priority"
case Medium: return "medium priority"
case High: return "high priority"
default:
return fmt.Sprintf("unknown priority: %d", p)
}
}
func main() {
var p Priority // zero value = 0
fmt.Println(describeTask(p)) // "unset/default"
fmt.Println(describeTask(Low)) // "low priority"
}
28. switch with Error Types¶
package main
import (
"errors"
"fmt"
"io"
"net"
)
// Using errors.As with switch alternative
func categorizeError(err error) string {
if err == nil {
return "no error"
}
var netErr *net.OpError
var pathErr *net.AddrError
switch {
case errors.Is(err, io.EOF):
return "end of stream"
case errors.Is(err, io.ErrUnexpectedEOF):
return "truncated data"
case errors.As(err, &netErr):
return "network error: " + netErr.Op
case errors.As(err, &pathErr):
return "address error: " + pathErr.Addr
default:
return "unknown: " + err.Error()
}
}
func main() {
errors_list := []error{
nil,
io.EOF,
fmt.Errorf("wrapped: %w", io.EOF),
fmt.Errorf("other error"),
}
for _, e := range errors_list {
fmt.Printf("%v -> %s\n", e, categorizeError(e))
}
}
29. Performance: Switch vs Map for Different Sizes¶
package main
import "testing"
// For N=5: switch is faster (no hash overhead)
// For N=50: comparable
// For N=500: map becomes faster
var cases5 = map[string]int{
"a": 1, "b": 2, "c": 3, "d": 4, "e": 5,
}
func switch5(s string) int {
switch s {
case "a": return 1
case "b": return 2
case "c": return 3
case "d": return 4
case "e": return 5
}
return 0
}
func map5(s string) int {
return cases5[s]
}
var sink int
func BenchmarkSwitch5(b *testing.B) {
for i := 0; i < b.N; i++ {
sink = switch5("c")
}
}
func BenchmarkMap5(b *testing.B) {
for i := 0; i < b.N; i++ {
sink = map5("c")
}
}
// switch5 typically wins for small N due to no hash computation
30. Debugging Guide and Common Pitfalls¶
| Pitfall | Example | Fix |
|---|---|---|
| Missing default | No default for new enum values | Always add default |
| Duplicate case | case 1: ... case 1: | Compile error — fix duplicate |
| Wrong fallthrough | Unexpected behavior | Use explicit cases or no fallthrough |
| Type case not specific enough | case error: matches too much | Order more specific interfaces first |
| Zero value unhandled | New variable with zero value hits default | Handle zero explicitly |
| String case sensitivity | case "Admin" misses "admin" | Normalize with strings.ToLower |
| Float comparison | case 0.1 + 0.2: may not match 0.3 | Use expressionless switch with epsilon |
| Shadowed variable | switch v := v.(type) shadows outer v | Use different name if needed |