If, Else & Switch
Go's control flow is explicit and readable.
Introduction
Go's control flow is explicit and readable. if statements support initialization statements (if err := fn(); err != nil), eliminating temp variables. switch auto-breaks (no fallthrough unless specified) and supports expression-less switches for clean multi-branch logic.
Go 1.22 enhanced switch to accept any comparable type and improved for-loop variable scoping. Pattern-style switches on type are done via type switches — a distinct construct covered with interfaces later.
Mastering guard clauses (early return on error), switch on enums, and avoiding deep nesting is essential for readable handlers, middleware, and service methods interviewers will review in take-home assignments.
The story
An API gateway health checker classifies upstream responses before routing traffic: 2xx is healthy, 429 triggers backoff, 5xx marks the origin degraded. SRE dashboards at companies like Stripe use similar branching logic — guard clauses reject malformed status codes before severity rules run.
switch on HTTP status ranges keeps the routing table readable compared to deeply nested if chains that become unmaintainable across dozens of microservices.
Understanding the topic
Key concepts
- if init; condition { } — init statement scoped to if/else block.
- else if chains handle multiple conditions; else is optional.
- switch expression { case val: } — no break needed; fallthrough is explicit.
- switch without expression equals switch true — cleaner than if-else chains.
- case supports comma-separated values: case 1, 2, 3:
- Type switch: switch v := x.(type) { case int: } for interface values.
flowchart TDStart([Condition]) --> Check{if true?}Check -->|yes| TrueBlock[Execute]Check -->|no| FalseBlock[Else]TrueBlock --> Merge[Continue]FalseBlock --> Merge
Step-by-step explanation
- Evaluate if condition; execute first matching block.
- Init statement in if runs once; variables scoped to if/else.
- switch evaluates cases top-to-bottom; first match executes.
- default case runs when no case matches.
- fallthrough continues to next case — rarely used, document why.
- Type switch binds variable to concrete type in each case.
Practical code example
HTTP-style routing logic with if-init error pattern and switch on status:
package mainimport "fmt"type Status stringconst (StatusActive Status = "active"StatusPending Status = "pending"StatusDisabled Status = "disabled")func describeAccount(status Status, balance float64) string {switch status {case StatusActive:if balance <= 0 {return "active but zero balance"}return fmt.Sprintf("active with balance %.2f", balance)case StatusPending:return "awaiting verification"case StatusDisabled:return "account disabled"default:return "unknown status"}}func main() {fmt.Println(describeAccount(StatusActive, 150.75))fmt.Println(describeAccount(StatusPending, 0))}
Line-by-line code explanation
if code < 100 || code > 599is a guard clause rejecting invalid HTTP status codes.return HealthUnknownexits early before main classification logic runs.switch { case code >= 200 && code < 300:uses a tagless switch for range matching.return HealthOKhandles the healthy arm without extra nesting.case code == 429:matches rate-limit responses for special retry handling.case code >= 500:catches all server errors in a single arm.default:provides safe fallback behavior for unexpected codes.switch tier := classify(code); tier { ... }assigns a switch result to a variable in one expression.
Key takeaway: if init; err != nil is the idiomatic error check. switch on typed constants (iota enums) is cleaner than long if-else chains.
Real-world use
Where you'll use this in production
- HTTP middleware branching on method, path, and headers.
- Validation pipelines with early return on first failure.
- State machine transitions in order processing services.
- Error classification switching on error types or codes.
Best practices
- Use guard clauses — return early instead of deep nesting.
- Prefer switch over long if-else chains for discrete values.
- Always include default case in switch for exhaustiveness awareness.
- Use if err := ...; err != nil immediately after calls returning error.
- Avoid fallthrough unless documenting C-style intentional fall-through.
- Extract complex conditions into well-named boolean variables.
Common mistakes
- Using switch with fallthrough accidentally — Go breaks by default unlike C.
- Comparing floats with == in conditions — use epsilon or integer cents.
- Deep nesting beyond 3 levels — refactor to functions or early return.
- Forgetting braces on if — Go requires them always.
- Type switch without ok check on type assertion elsewhere.
Advanced interview questions
Q1Beginnerif init statement purpose?
Q2BeginnerDoes Go switch fall through?
Q3IntermediateSwitch without expression?
Q4IntermediateRefactor nested ifs in legacy handler?
Q5AdvancedModel complex insurance rules maintainably in Go.
Summary
if init; cond scopes temporary variables cleanly for error checks. switch auto-breaks; use for enums and multi-value cases. Guard clauses and early return beat deep nesting. Type switches handle interface{} or any values safely. Next lesson: for loops — Go's only looping construct.