Foundations Updated 2026-09 View as Markdown

Conditions and Switch

Go has one conditional statement and one switch, and both do more than their equivalents elsewhere.

go
if score > 90 {
    fmt.Println("excellent")
} else if score > 70 {
    fmt.Println("good")
} else {
    fmt.Println("keep going")
}

No parentheses around the condition, and the braces are mandatory even for one line. The opening brace must be on the same line — Go's automatic semicolon insertion breaks the statement otherwise, which is why gofmt will not let you move it.

The statement-scoped if#

The idiom you will write most often in Go:

go
if err := doSomething(); err != nil {
    return fmt.Errorf("do something: %w", err)
}
// err does not exist here

The initialiser runs first, then the condition. Anything it declares is scoped to the if and its else branches — which keeps short-lived variables like err out of the enclosing function.

go
if user, err := findUser(id); err != nil {
    return err
} else {
    fmt.Println(user.Name)      // both user and err visible here
}

In practice you rarely need the else — returning early is the dominant Go style:

go
user, err := findUser(id)
if err != nil {
    return err
}
fmt.Println(user.Name)          // the happy path stays unindented

switch#

Go's switch has no automatic fallthrough, so no break is needed:

go
switch status {
case "pending":
    fmt.Println("waiting")
case "shipped", "delivered":     // several values in one case
    fmt.Println("on its way")
default:
    fmt.Println("unknown")
}

switch with no condition#

A switch with nothing after it means switch true, which replaces a long if/else chain:

go
switch {
case score > 90:
    grade = "A"
case score > 70:
    grade = "B"
default:
    grade = "C"
}

This is idiomatic Go and usually reads better than the equivalent else if ladder.

switch with an initialiser#

go
switch hour := time.Now().Hour(); {
case hour < 12:
    fmt.Println("morning")
case hour < 18:
    fmt.Println("afternoon")
default:
    fmt.Println("evening")
}

fallthrough#

Available, and rare:

go
switch n {
case 1:
    fmt.Println("one")
    fallthrough        // continue into the next case regardless of its condition
case 2:
    fmt.Println("two")
}
// n == 1 prints both

Type switches#

A switch on the dynamic type of an interface value. This is how you handle a value that could be several things:

go
func describe(v any) string {
    switch x := v.(type) {
    case nil:
        return "nothing"
    case int:
        return fmt.Sprintf("int %d", x*2)      // x is an int here
    case string:
        return "string of length " + strconv.Itoa(len(x))
    case []int:
        return fmt.Sprintf("%d ints", len(x))
    case error:
        return "error: " + x.Error()
    default:
        return fmt.Sprintf("unhandled %T", x)
    }
}

Inside each case, x has that specific type. %T in the default branch prints the actual type, which is invaluable when debugging.

Comparison and logic#

go
==  !=  <  <=  >  >=
&&  ||  !

&& and || short-circuit. Two Go-specific points:

There is no ternary operator. Write the if, or a small helper:

go
label := "off"
if enabled {
    label = "on"
}

There are no truthy values. A condition must be a bool:

go
if count { }        // error: non-boolean condition
if count != 0 { }   // fine
if name != "" { }   // fine

That strictness removes a whole class of bug found in dynamically typed languages — and it is one reason generated Go is less likely to be subtly wrong than generated JavaScript.

Exercise#

go
package main

import "fmt"

func main() {
	scores := []int{95, 72, 58, 88, 100}
	// For each score print "<score>: <grade>" where grade is
	//   A for 90+, B for 80-89, C for 70-79, F below 70.
	// Use a conditionless switch.
	fmt.Println(scores)
}

Common questions#

Why is there no ternary operator?#

A deliberate omission — the Go authors judged that nested ternaries hurt readability more than the brevity helps. The three-line if is the intended form, and in practice it is rarely the thing that makes Go verbose.

When should I use a type switch?#

When handling a value whose type genuinely varies — parsing arbitrary JSON, walking an AST, or inspecting an error chain. If you find yourself type-switching on your own types regularly, an interface with a method is usually the better design.

Should I use else?#

Sparingly. The dominant Go style is to handle the error case and return early, leaving the happy path unindented. A function whose main logic sits inside an else block is usually one early return away from being clearer.

Get the Go agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for Go. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.