if Statement — Middle Level¶
1. Design Philosophy: Why Go's if Works the Way It Does¶
Go's if statement was designed with explicit preferences rooted in correctness and readability:
- No parentheses: reduces visual noise, distinguishes Go from C/Java
- Required braces: prevents off-by-one bugs (Apple's "goto fail" SSL bug, 2014, was caused by missing braces)
- Init statement: enables idiomatic error checking without polluting outer scope
- No ternary
?:: encourages explicit, readable branching over concise-but-cryptic one-liners - Bool only: no implicit int→bool or string→bool — prevents
if nwheren==0is unexpectedly false
// The Apple SSL "goto fail" bug (C, 2014) — required braces prevent this in Go:
// if (err = hashUpdate(&hashCtx, &data)) != 0)
// goto fail;
// goto fail; // always executes! Missing braces made it unconditional
2. When to Use if vs switch¶
// Use if when:
// - Condition is complex (multiple operators)
// - Different conditions on different variables
// - Error checking (nil/non-nil pattern)
if err != nil { return err }
if len(s) > 0 && s[0] == 'A' { process(s) }
// Use switch when:
// - Multiple equality checks on the SAME variable/expression
// - 4+ branches
// - Type switching
switch status {
case "active": doActive()
case "pending": doPending()
case "inactive": doInactive()
default: doDefault()
}
3. The Init Statement: Scope Management Tool¶
The init statement is not just syntactic sugar — it prevents scope pollution.
// WITHOUT init statement: err leaks into outer scope
err := doA()
if err != nil { return err }
err = doB() // must use = not :=
if err != nil { return err }
// WITH init statement: each err is locally scoped
if err := doA(); err != nil { return err }
if err := doB(); err != nil { return err }
// No stale err variable in scope between calls
Why this matters: In long functions, a leaked err variable from one call can shadow errors from subsequent calls if the developer accidentally uses = instead of :=.
4. Why Go Has No Ternary Operator¶
The Go authors explicitly rejected ternary ?::
- Complexity: Nested ternaries
a ? b ? c : d : eare nearly unreadable - Debugging: Can't place a breakpoint inside a ternary expression
- Ambiguity: Different precedence rules across languages cause bugs
- Readability:
if-elseis always unambiguous
// Go's way for conditional assignment:
var status string
if isActive {
status = "active"
} else {
status = "inactive"
}
// Or: use a helper function for repeated patterns
func cond(ok bool, a, b string) string {
if ok { return a }
return b
}
status := cond(isActive, "active", "inactive")
5. Guard Clauses: The Psychological Model¶
Guard clauses work because of how humans read code:
Mental model — nested approach:
"To understand what happens, I must first track all conditions to reach the main logic... and keep them in working memory while reading."
Mental model — guard clause approach:
"The first few lines tell me what's invalid and exits early. Past that, I'm in the valid case."
// Nested (reader tracks multiple conditions simultaneously):
func CreateOrder(req OrderRequest) (*Order, error) {
if req.UserID > 0 {
if len(req.Items) > 0 {
if req.PaymentMethod != "" {
// main logic 3 levels deep
}
}
}
return nil, errors.New("invalid")
}
// Guard clauses (reader handles one concern at a time):
func CreateOrder(req OrderRequest) (*Order, error) {
if req.UserID <= 0 { return nil, errors.New("invalid user") }
if len(req.Items) == 0 { return nil, errors.New("no items") }
if req.PaymentMethod == "" { return nil, errors.New("no payment method") }
// Main logic — no condition tracking needed
order := &Order{UserID: req.UserID, Items: req.Items}
return order, nil
}
6. Short-Circuit Evaluation — Safety Patterns¶
// Nil guard before field access:
if user != nil && user.IsActive {
process(user) // safe: user.IsActive only accessed if user is non-nil
}
// Cheap check before expensive operation:
if quickSanityCheck(data) && expensiveParsing(data) {
// expensiveParsing only called if quick check passes
}
// Lazy initialization:
if cache != nil || initCache() != nil {
use(cache)
}
// BEWARE: don't use short-circuit for side effects
// This creates hidden control flow:
if writeLog() || sendAlert() {
// sendAlert only called if writeLog returns false — confusing!
}
7. Error Handling with errors.Is and errors.As¶
var ErrNotFound = errors.New("not found")
func getUser(id int) (*User, error) {
row := db.QueryRow("SELECT * FROM users WHERE id = $1", id)
if err := row.Scan(&user); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, fmt.Errorf("user %d: %w", id, ErrNotFound)
}
return nil, fmt.Errorf("query user %d: %w", id, err)
}
return &user, nil
}
// Caller dispatches on error type:
user, err := getUser(42)
if err != nil {
switch {
case errors.Is(err, ErrNotFound):
http.Error(w, "not found", 404)
case errors.Is(err, ErrPermission):
http.Error(w, "forbidden", 403)
default:
log.Printf("unexpected: %v", err)
http.Error(w, "internal error", 500)
}
return
}
8. Anti-Pattern: else After return¶
// ANTI-PATTERN: unnecessary else
func getUser(id int) (*User, error) {
u, err := db.Find(id)
if err != nil {
return nil, err
} else { // else is unnecessary!
return u, nil
}
}
// IDIOMATIC: drop the else
func getUser(id int) (*User, error) {
u, err := db.Find(id)
if err != nil {
return nil, err
}
return u, nil
}
The revive and staticcheck linters flag else after return, continue, break, or goto.
9. Anti-Pattern: Boolean Blindness¶
// ANTI-PATTERN: boolean return hides intent
func canVote(age int) bool {
if age >= 18 {
return true
}
return false
// Simplify to: return age >= 18
}
// ANTI-PATTERN: condition restated in if
isValid := validateUser(user)
if isValid == true { } // use: if isValid { }
if isValid == false { } // use: if !isValid { }
// Better: return descriptive errors when more context is needed
func checkVotingEligibility(age, registrationYear int) error {
if age < 18 {
return fmt.Errorf("voter must be 18+, got %d", age)
}
if registrationYear > currentYear {
return fmt.Errorf("registration year %d is in the future", registrationYear)
}
return nil
}
10. Alternative: Function Table for Large Dispatch¶
// 5+ branches — consider a function table instead of if-else if
type HandlerFn func(ctx context.Context, req Request) Response
var actionHandlers = map[string]HandlerFn{
"create": handleCreate,
"update": handleUpdate,
"delete": handleDelete,
"list": handleList,
"get": handleGet,
}
func dispatch(ctx context.Context, action string, req Request) Response {
h, ok := actionHandlers[action]
if !ok {
return Response{Error: fmt.Sprintf("unknown action: %s", action)}
}
return h(ctx, req)
}
vs if-else if chain:
// Hard to maintain at 5+ cases:
if action == "create" {
return handleCreate(ctx, req)
} else if action == "update" {
return handleUpdate(ctx, req)
} else if action == "delete" {
return handleDelete(ctx, req)
// ... more cases
}
11. if in the Standard Library Error Pattern¶
The Go standard library consistently uses the guard-clause pattern:
// From net/http (simplified):
func (h *Handler) ServeHTTP(w ResponseWriter, r *Request) {
if r.Method == "" {
r.Method = "GET"
}
if r.URL == nil {
http.Error(w, "missing URL", 400)
return
}
if r.Body == http.NoBody {
// handle no-body case
}
// main logic
}
12. Debugging if Conditions¶
Technique 1: Extract condition to named variable
// Hard to debug:
if user.Age >= 18 && user.IsActive && !user.IsBlocked && user.Balance > 0 {
grantAccess()
}
// Debuggable:
isAdult := user.Age >= 18
isActive := user.IsActive
notBlocked := !user.IsBlocked
hasFunds := user.Balance > 0
log.Printf("access check: adult=%v active=%v notBlocked=%v funds=%v",
isAdult, isActive, notBlocked, hasFunds)
if isAdult && isActive && notBlocked && hasFunds {
grantAccess()
}
Technique 2: Use delve
dlv debug ./cmd/main
(dlv) break handlers.go:42
(dlv) continue
(dlv) print user
(dlv) print user.Age >= 18
13. Evolution of Go's if Statement¶
| Era | Change |
|---|---|
| Go 1.0 (2012) | Current if syntax established (no changes since) |
| Go 1.13 (2019) | errors.Is/errors.As changed how errors are checked in if conditions |
| Go 1.18 (2022) | Generics didn't change if, but type constraints use similar boolean logic |
| Go 1.21 (2023) | min/max builtins reduce some if usage for comparisons |
// Go 1.21: use min/max instead of if-else for simple comparisons
// Before:
var result int
if a < b {
result = a
} else {
result = b
}
// After (Go 1.21+):
result := min(a, b)
14. Language Comparison: Error Handling¶
# Python: exceptions (implicit control flow)
try:
result = parse(data)
except ValueError as e:
handle_error(e)
// Java: checked exceptions (forced handling)
try {
result = parse(data);
} catch (ParseException e) {
handleError(e);
}
// Rust: Result type with ? operator (concise propagation)
let result = parse(data)?; // ? propagates error up
// Go: explicit if check (visible, no hidden control flow)
result, err := parse(data)
if err != nil {
return fmt.Errorf("parse: %w", err)
}
Go's approach makes error propagation visible at every call site. Verbose but eliminates surprise control flow jumps.
15. if with Context Cancellation¶
func processWithContext(ctx context.Context, items []Item) error {
for _, item := range items {
// Check context before each iteration
if err := ctx.Err(); err != nil {
return fmt.Errorf("processing cancelled: %w", err)
}
if err := processItem(ctx, item); err != nil {
return fmt.Errorf("item %v: %w", item.ID, err)
}
}
return nil
}
16. Scope Issues with := in Init Statement¶
x := 10
// Shadowing (creates NEW x in if scope):
if x := x + 5; x > 10 {
fmt.Println("inner x:", x) // 15
}
fmt.Println("outer x:", x) // 10 — unchanged!
// Intentional use (error chain):
err := firstOp()
if err != nil {
if err := secondOp(); err != nil {
// inner err is secondOp's error
return err
}
return err // firstOp's error
}
17. Performance: Branch Prediction¶
Modern CPUs predict branch outcomes. Mispredictions cost ~15 clock cycles.
// For predictable branches (hot path always taken):
// Branch predictor learns — minimal cost after warmup
// For unpredictable branches (50/50):
// ~15 cycle penalty per misprediction
// Go compiler optimization for simple branches:
func max(a, b int) int {
if a > b { return a }
return b
}
// May compile to CMOV (conditional move) — no branch, no misprediction penalty
// Order conditions: most likely first for early exit
if !criticalCheck() { // fast common path
return
}
if slowCheck() { } // only evaluated when criticalCheck passes
18. Testing All if Branches¶
// Every branch of an if should be tested:
func TestDivide(t *testing.T) {
// Test: error branch (b == 0)
_, err := divide(10, 0)
if err == nil {
t.Error("expected error for division by zero")
}
// Test: normal branch (b != 0)
result, err := divide(10, 2)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if result != 5 {
t.Errorf("10/2 = %v, want 5", result)
}
}
// Check branch coverage:
// go test -coverprofile=coverage.out ./...
// go tool cover -func=coverage.out | grep -v "100.0%"
19. if in Request Validation Pipelines¶
type Validator func(*http.Request) error
func validateRequest(r *http.Request, validators ...Validator) error {
for _, v := range validators {
if err := v(r); err != nil {
return err
}
}
return nil
}
func requireMethod(method string) Validator {
return func(r *http.Request) error {
if r.Method != method {
return fmt.Errorf("method %s not allowed", r.Method)
}
return nil
}
}
func requireContentType(ct string) Validator {
return func(r *http.Request) error {
if !strings.HasPrefix(r.Header.Get("Content-Type"), ct) {
return fmt.Errorf("content-type must be %s", ct)
}
return nil
}
}
func handler(w http.ResponseWriter, r *http.Request) {
if err := validateRequest(r,
requireMethod("POST"),
requireContentType("application/json"),
); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
}
20. if Linting Rules¶
# .golangci.yml
linters:
enable:
- revive # "else-return": flags else after return
- gocritic # "sloppyReassign", "boolExprSimplify"
- staticcheck # "SA4003": unreachable conditions
- errcheck # ensures all errors are checked
Common findings:
// revive: redundant else
if cond { return true } else { return false }
// → return cond
// gocritic: equalFold
if strings.ToLower(s) == "go" { }
// → if strings.EqualFold(s, "go") { }
// staticcheck SA4003: condition always false
var x uint8
if x < 0 { } // uint8 is always >= 0
21. if vs select for Channel Operations¶
// if for single channel operation:
if val, ok := <-ch; ok {
process(val)
}
// select for multiple channels (non-blocking):
select {
case val := <-ch:
process(val)
case <-ctx.Done():
return ctx.Err()
default:
// non-blocking: nothing ready
}
22. Mixing if with defer¶
func processFile(path string) (err error) {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open: %w", err)
}
// Named return + defer captures close error
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = fmt.Errorf("close: %w", cerr)
}
}()
if _, err = f.Write([]byte("data")); err != nil {
return fmt.Errorf("write: %w", err)
}
return nil
}
23. The errors.As Pattern in if¶
// Type-specific error handling with errors.As:
func handleError(err error) (statusCode int, message string) {
var notFound *NotFoundError
var validation *ValidationError
var timeout interface{ Timeout() bool }
switch {
case errors.As(err, ¬Found):
return 404, fmt.Sprintf("not found: %s", notFound.Resource)
case errors.As(err, &validation):
return 422, fmt.Sprintf("invalid: %s", validation.Message)
case errors.As(err, &timeout) && timeout.Timeout():
return 504, "gateway timeout"
default:
return 500, "internal server error"
}
}
24. if in Go Generics¶
// Generic function using if:
func Filter[T any](items []T, predicate func(T) bool) []T {
var result []T
for _, item := range items {
if predicate(item) {
result = append(result, item)
}
}
return result
}
// Type constraint check at runtime (using reflect or type assertion):
func MustBePositive[T int | float64](v T) T {
if v <= 0 {
panic(fmt.Sprintf("expected positive value, got %v", v))
}
return v
}
25. Chaining vs. Nesting: Visual Comparison¶
NESTED (avoid): CHAINED GUARD CLAUSES (prefer):
┌─────────────────────────┐ if !A { return errA }
│ if A { │ if !B { return errB }
│ if B { │ if !C { return errC }
│ if C { │
│ // main logic │ // main logic here
│ } │
│ } │
│ } │
└─────────────────────────┘
26. The if Statement Cyclomatic Complexity¶
Each if, else if, for, case, and &&/|| adds to cyclomatic complexity. Tools measure it:
Keep functions with many if statements simple by: 1. Extracting validation into helper functions 2. Using early returns (guard clauses) 3. Moving branching logic into a table
27. Pattern: if for Feature Flags¶
type FeatureFlags struct {
NewCheckout bool
BetaSearch bool
DarkMode bool
}
func handleCheckout(w http.ResponseWriter, r *http.Request, flags FeatureFlags) {
if flags.NewCheckout {
newCheckoutHandler(w, r)
return
}
legacyCheckoutHandler(w, r)
}
func renderPage(flags FeatureFlags) string {
if flags.DarkMode {
return renderWithTheme("dark")
}
return renderWithTheme("light")
}
28. if Without Explicit else — The Continuation Pattern¶
// Pattern: if terminates (return/panic/continue/break), else is implicit
func processItems(items []Item) []Result {
var results []Result
for _, item := range items {
if item.IsExpired() {
continue // skip expired — no else needed
}
if !item.IsValid() {
log.Printf("invalid item %v, skipping", item.ID)
continue
}
// Only valid, non-expired items reach here
results = append(results, process(item))
}
return results
}
29. if in Benchmark Tests¶
func BenchmarkClassify(b *testing.B) {
testValues := []int{-100, 0, 5, 50, 500}
b.ReportAllocs()
for i := 0; i < b.N; i++ {
// Use all paths to benchmark all branches
for _, v := range testValues {
if v < 0 {
_ = "negative"
} else if v == 0 {
_ = "zero"
} else {
_ = "positive"
}
}
}
}
30. Complete Pattern: Multi-Layer if Validation¶
// A production-quality request handler using guard clauses throughout
func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
// Layer 1: HTTP validation
if r.Method != http.MethodPost {
writeError(w, 405, "method not allowed")
return
}
if r.Header.Get("Content-Type") != "application/json" {
writeError(w, 415, "unsupported media type")
return
}
// Layer 2: Request parsing
var req CreateOrderRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, 400, fmt.Sprintf("invalid JSON: %v", err))
return
}
// Layer 3: Input validation
if req.UserID <= 0 {
writeError(w, 422, "user_id must be positive")
return
}
if len(req.Items) == 0 {
writeError(w, 422, "items cannot be empty")
return
}
// Layer 4: Business logic
order, err := h.orderService.Create(r.Context(), req)
if err != nil {
if errors.Is(err, ErrUserNotFound) {
writeError(w, 404, "user not found")
return
}
if errors.Is(err, ErrInsufficientStock) {
writeError(w, 409, "insufficient stock")
return
}
h.logger.Error("create order failed", "err", err)
writeError(w, 500, "internal error")
return
}
// Success path — reached only after all guards pass
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(order)
}