CHAPTER 09 · CORE LANGUAGE

Conditional Flow

Decisions! Go's conditional toolkit is small: if, else, and switch. There are no parentheses around conditions, no ternary operator, and braces are mandatory. The small surface area is intentional: there are fewer ways to write the same thing.

Learning objectives

  • Write if, if-else, and chained else if statements.
  • Use the short-statement form to scope a variable to its if.
  • Choose between switch and an if-else chain.
  • Recognize the tagless switch idiom and the fallthrough keyword.
  • Spot a type switch and know what it's for.

if / else

package main

import "fmt"

func main() {
    age := 18

    if age >= 18 {
        fmt.Println("adult")
    } else if age >= 13 {
        fmt.Println("teen")
    } else {
        fmt.Println("child")
    }
}

Things to notice:

  • No parentheses around the condition.
  • Braces required, even for one-line bodies.
  • The opening { must be on the same line as the if/else.
  • The condition must be a bool: no truthy/falsy.
!
Curly braces aren't optional Coming from C? You can't omit braces around a single-statement body. This rules out a class of famous bugs where missing braces silently change behavior.

Short statement form

Go lets you put an initialization statement before the condition, separated by a semicolon. The initialized variable is scoped to the if/else blocks:

if n := compute(); n > 0 {
    fmt.Println("positive:", n)
} else {
    fmt.Println("non-positive:", n)
}
// n is NOT in scope here

This is incredibly common in idiomatic Go for handling errors:

if err := saveUser(u); err != nil {
    return fmt.Errorf("save: %w", err)
}
// err is gone, it lived only inside the if

Tightly scoping the error variable avoids "leaking" it into the rest of the function and keeps your code easy to follow.

switch

For matching one value against many candidates, switch is cleaner than an if-else if ladder:

switch day {
case "Mon", "Tue", "Wed", "Thu", "Fri":
    fmt.Println("weekday")
case "Sat", "Sun":
    fmt.Println("weekend")
default:
    fmt.Println("unknown day")
}

Two important differences from C/Java:

  1. No implicit fallthrough. Each case ends after its body.
  2. Multiple values per case are comma-separated, no need for stacked case labels.

Cases can be expressions, too, they don't have to be constants:

switch n := rand.Intn(100); {
case n < 50:
    fmt.Println("small")
case n < 90:
    fmt.Println("medium")
default:
    fmt.Println("large")
}

Tagless switch

Drop the value and you get a tagless switch: a much cleaner replacement for long if-else if chains:

switch {
case temp < 0:
    fmt.Println("freezing")
case temp < 10:
    fmt.Println("cold")
case temp < 25:
    fmt.Println("comfortable")
default:
    fmt.Println("hot")
}

The tagless switch is one of those small Go niceties that, once you use it, you start wishing other languages had. It's the same as switch true.

fallthrough (rarely needed)

If you really want C-style fall-through into the next case, use fallthrough:

switch n {
case 1:
    fmt.Println("one")
    fallthrough
case 2:
    fmt.Println("two")    // also runs when n == 1
}

Use it sparingly. Most cases where you'd reach for it are clearer written with multiple comma-separated case values.

Type switch (preview)

A type switch matches on the dynamic type of an interface value, not on its value. We'll meet interfaces properly in Chapter 21, but here's the syntax for recognition:

func describe(v any) {
    switch x := v.(type) {
    case int:
        fmt.Printf("int %d\n", x)
    case string:
        fmt.Printf("string %q (len %d)\n", x, len(x))
    case []byte:
        fmt.Printf("byte slice (len %d)\n", len(x))
    case nil:
        fmt.Println("nil")
    default:
        fmt.Printf("unknown type %T\n", x)
    }
}

func main() {
    describe(42)
    describe("hello")
    describe([]byte{1, 2, 3})
    describe(nil)
}

The v.(type) syntax only works inside a switch. Inside each case, x already has the matched type, no further assertions needed.

Style notes

  • Prefer early returns over deep nesting. Instead of if ok { ... } else { return err }, write if !ok { return err } first and let the happy path stay flat.
  • Don't compare booleans to true/false. Write if isReady, not if isReady == true.
  • Use a switch when you have more than two branches on the same value, easier to read than nested if-else.

Check your understanding

Practice exercises

EXERCISE 1

Grade calculator

Write a function grade(score int) string that returns "A" for 90+, "B" for 80–89, "C" for 70–79, "D" for 60–69, and "F" otherwise. Use a tagless switch.

Show one possible solution
package main

import "fmt"

func grade(score int) string {
    switch {
    case score >= 90:
        return "A"
    case score >= 80:
        return "B"
    case score >= 70:
        return "C"
    case score >= 60:
        return "D"
    default:
        return "F"
    }
}

func main() {
    fmt.Println(grade(95))   // A
    fmt.Println(grade(75))   // C
    fmt.Println(grade(40))   // F
}
EXERCISE 2

Type-switch on `any`

Write a function describe(v any) string that returns a short string describing the value: "int N", "float F", "string S (len L)", "bool B", or "unknown".

Show one possible solution
package main

import "fmt"

func describe(v any) string {
    switch x := v.(type) {
    case int:
        return fmt.Sprintf("int %d", x)
    case float64:
        return fmt.Sprintf("float %g", x)
    case string:
        return fmt.Sprintf("string %q (len %d)", x, len(x))
    case bool:
        return fmt.Sprintf("bool %t", x)
    default:
        return "unknown"
    }
}

func main() {
    for _, v := range []any{1, 1.5, "hi", true, []int{1, 2}} {
        fmt.Println(describe(v))
    }
}

Further reading

Decisions made.