In the previous tutorial, you learned about functions and error handling. Now it is time to learn how to control the flow of your program with if, switch, and for.

Go keeps control flow simple. There is only one loop keyword: for. No while, no do-while. Just for. The switch statement is also simpler and more powerful than in most languages.

if / else

The basic if statement works like most languages. But Go does not need parentheses around the condition:

package main

import "fmt"

func main() {
    age := 20

    if age >= 18 {
        fmt.Println("You are an adult")
    } else {
        fmt.Println("You are a minor")
    }
}

Output:

You are an adult

You can chain multiple conditions with else if:

package main

import "fmt"

func classifyTemperature(temp int) string {
    if temp < 0 {
        return "Freezing"
    } else if temp < 10 {
        return "Cold"
    } else if temp < 20 {
        return "Cool"
    } else if temp < 30 {
        return "Warm"
    } else {
        return "Hot"
    }
}

func main() {
    temps := []int{-5, 5, 15, 25, 35}
    for _, t := range temps {
        fmt.Printf("%d°C is %s\n", t, classifyTemperature(t))
    }
}

Output:

-5°C is Freezing
5°C is Cold
15°C is Cool
25°C is Warm
35°C is Hot

if with Short Statement

This is one of Go’s best features. You can put a short statement before the condition. The variable you declare is only available inside the if block:

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // The short statement declares err, which is only available in this if block
    if num, err := strconv.Atoi("42"); err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Number:", num)
    }

    // num and err are NOT available here

    // This is the most common pattern — error checking
    if err := doSomething(); err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Success")
}

func doSomething() error {
    return nil
}

Output:

Number: 42
Success

The short statement pattern keeps error-handling code compact. You will see it everywhere in Go code:

if err := os.Remove("file.txt"); err != nil {
    fmt.Println("Could not delete file:", err)
}

switch

Go’s switch statement is cleaner than in most languages. There is no fall-through by default, so you don’t need break statements:

package main

import "fmt"

func dayType(day string) string {
    switch day {
    case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
        return "Weekday"
    case "Saturday", "Sunday":
        return "Weekend"
    default:
        return "Unknown"
    }
}

func main() {
    fmt.Println(dayType("Monday"))   // Weekday
    fmt.Println(dayType("Saturday")) // Weekend
    fmt.Println(dayType("Holiday"))  // Unknown
}

Notice: multiple values in one case are separated by commas. No need for multiple case statements.

switch Without a Condition

You can use switch without a condition. It acts like a chain of if-else statements, but it is easier to read:

package main

import "fmt"

func classifyScore(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() {
    scores := []int{95, 85, 72, 63, 45}
    for _, s := range scores {
        fmt.Printf("Score %d: Grade %s\n", s, classifyScore(s))
    }
}

Output:

Score 95: Grade A
Score 85: Grade B
Score 72: Grade C
Score 63: Grade D
Score 45: Grade F

This is a very common pattern in Go. When you have many conditions to check, a conditionless switch is cleaner than a chain of if-else.

switch with Short Statement

Just like if, switch can have a short statement:

package main

import (
    "fmt"
    "time"
)

func main() {
    switch hour := time.Now().Hour(); {
    case hour < 12:
        fmt.Println("Good morning!")
    case hour < 17:
        fmt.Println("Good afternoon!")
    default:
        fmt.Println("Good evening!")
    }
}

Type Switch

A type switch checks the type of a value. This is useful when working with interfaces:

package main

import "fmt"

func describe(value interface{}) string {
    switch v := value.(type) {
    case int:
        return fmt.Sprintf("Integer: %d", v)
    case float64:
        return fmt.Sprintf("Float: %.2f", v)
    case string:
        return fmt.Sprintf("String: %q (length %d)", v, len(v))
    case bool:
        return fmt.Sprintf("Boolean: %t", v)
    default:
        return fmt.Sprintf("Unknown type: %T", v)
    }
}

func main() {
    fmt.Println(describe(42))
    fmt.Println(describe(3.14))
    fmt.Println(describe("hello"))
    fmt.Println(describe(true))
}

Output:

Integer: 42
Float: 3.14
String: "hello" (length 5)
Boolean: true

The value.(type) syntax only works inside a switch statement. The variable v gets the correct type in each case branch.

fallthrough

By default, Go’s switch does not fall through to the next case. If you want fall-through behavior (like C or Java), use the fallthrough keyword:

package main

import "fmt"

func main() {
    num := 1
    switch num {
    case 1:
        fmt.Println("One")
        fallthrough
    case 2:
        fmt.Println("Two")
        fallthrough
    case 3:
        fmt.Println("Three")
    }
}

Output:

One
Two
Three

In practice, you rarely need fallthrough. Most Go code does not use it.

for — The Only Loop

Go has only one loop keyword: for. But it can do everything that for, while, and do-while do in other languages.

Classic for Loop

package main

import "fmt"

func main() {
    // Classic: init; condition; post
    for i := 0; i < 5; i++ {
        fmt.Printf("i = %d\n", i)
    }
}

Output:

i = 0
i = 1
i = 2
i = 3
i = 4

for as while

Drop the init and post statements. Now for acts like while:

package main

import "fmt"

func main() {
    count := 1
    for count <= 5 {
        fmt.Printf("count = %d\n", count)
        count++
    }
}

Output:

count = 1
count = 2
count = 3
count = 4
count = 5

Infinite Loop

Drop everything. Just for:

package main

import "fmt"

func main() {
    sum := 0
    for {
        sum++
        if sum >= 10 {
            break // Exit the loop
        }
    }
    fmt.Println("Sum:", sum)
}

Output:

Sum: 10

Use break to exit an infinite loop. This pattern is common for programs that run forever (like servers) or when the exit condition is complex.

for range — Iterating Over Collections

for range is the way to iterate over slices, maps, strings, and channels in Go:

package main

import "fmt"

func main() {
    // Range over a slice
    fruits := []string{"apple", "banana", "cherry"}
    for index, fruit := range fruits {
        fmt.Printf("%d: %s\n", index, fruit)
    }

    fmt.Println()

    // Range over a string (gives you runes)
    for i, ch := range "Hello" {
        fmt.Printf("%d: %c\n", i, ch)
    }

    fmt.Println()

    // Range over a map
    ages := map[string]int{
        "Alex":   25,
        "Sam":    30,
        "Jordan": 22,
    }
    for name, age := range ages {
        fmt.Printf("%s is %d years old\n", name, age)
    }
}

Output:

0: apple
1: banana
2: cherry

0: H
1: e
2: l
3: l
4: o

Alex is 25 years old
Sam is 30 years old
Jordan is 22 years old

If you only need the index:

for i := range fruits {
    fmt.Println(i)
}

If you only need the value:

for _, fruit := range fruits {
    fmt.Println(fruit)
}

for range with integers (Go 1.22+)

Since Go 1.22, you can use range with an integer:

package main

import "fmt"

func main() {
    // Range over an integer — same as for i := 0; i < 5; i++
    for i := range 5 {
        fmt.Printf("i = %d\n", i)
    }
}

Output:

i = 0
i = 1
i = 2
i = 3
i = 4

This is a clean way to loop a specific number of times.

break and continue

break exits the loop. continue skips to the next iteration:

package main

import "fmt"

func main() {
    // continue — skip even numbers
    fmt.Println("Odd numbers:")
    for i := 1; i <= 10; i++ {
        if i%2 == 0 {
            continue
        }
        fmt.Printf("%d ", i)
    }
    fmt.Println()

    // break — stop when we find the target
    fmt.Println("\nSearching for 7:")
    numbers := []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 7, 8}
    for i, n := range numbers {
        if n == 7 {
            fmt.Printf("Found 7 at index %d\n", i)
            break
        }
    }
}

Output:

Odd numbers:
1 3 5 7 9

Searching for 7:
Found 7 at index 10

Labels

Labels let you break out of nested loops:

package main

import "fmt"

func main() {
    // Find the first pair that sums to 10
outer:
    for i := 1; i <= 9; i++ {
        for j := 1; j <= 9; j++ {
            if i+j == 10 {
                fmt.Printf("Found: %d + %d = 10\n", i, j)
                break outer // Break out of BOTH loops
            }
        }
    }
    fmt.Println("Done")
}

Output:

Found: 1 + 9 = 10
Done

Without the label, break would only exit the inner loop. With break outer, it exits both loops.

A Complete Example

Here is a program that uses all the control flow concepts from this tutorial:

package main

import (
    "fmt"
    "strings"
)

func main() {
    fmt.Println("=== GO-5: Control Flow ===")
    fmt.Println()

    // FizzBuzz — a classic programming exercise
    fmt.Println("FizzBuzz (1-20):")
    for i := 1; i <= 20; i++ {
        switch {
        case i%15 == 0:
            fmt.Printf("%2d: FizzBuzz\n", i)
        case i%3 == 0:
            fmt.Printf("%2d: Fizz\n", i)
        case i%5 == 0:
            fmt.Printf("%2d: Buzz\n", i)
        default:
            fmt.Printf("%2d: %d\n", i, i)
        }
    }

    fmt.Println()

    // Count words in a sentence
    sentence := "Go is a simple and powerful language"
    words := strings.Fields(sentence)
    wordCount := make(map[string]int)

    for _, word := range words {
        lower := strings.ToLower(word)
        wordCount[lower]++
    }

    fmt.Println("Word counts:")
    for word, count := range wordCount {
        fmt.Printf("  %s: %d\n", word, count)
    }

    fmt.Println()

    // Find prime numbers using a labeled loop
    fmt.Println("Prime numbers (2-30):")
    for num := 2; num <= 30; num++ {
        isPrime := true
        for div := 2; div*div <= num; div++ {
            if num%div == 0 {
                isPrime = false
                break
            }
        }
        if isPrime {
            fmt.Printf("%d ", num)
        }
    }
    fmt.Println()
    fmt.Println()

    // Type switch example
    values := []interface{}{42, "hello", 3.14, true, nil}
    fmt.Println("Type descriptions:")
    for _, v := range values {
        switch v := v.(type) {
        case int:
            fmt.Printf("  int: %d\n", v)
        case string:
            fmt.Printf("  string: %q\n", v)
        case float64:
            fmt.Printf("  float64: %.2f\n", v)
        case bool:
            fmt.Printf("  bool: %t\n", v)
        case nil:
            fmt.Println("  nil value")
        }
    }

    fmt.Println()

    // Simple number guessing game logic
    secret := 42
    guesses := []int{25, 50, 42, 30}
    fmt.Printf("Guessing game (secret: %d):\n", secret)
    for attempt, guess := range guesses {
        fmt.Printf("  Attempt %d: guessed %d — ", attempt+1, guess)
        switch {
        case guess < secret:
            fmt.Println("Too low!")
        case guess > secret:
            fmt.Println("Too high!")
        default:
            fmt.Println("Correct!")
        }
        if guess == secret {
            break
        }
    }
}

Output:

=== GO-5: Control Flow ===

FizzBuzz (1-20):
 1: 1
 2: 2
 3: Fizz
 4: 4
 5: Buzz
 6: Fizz
 7: 7
 8: 8
 9: Fizz
10: Buzz
11: 11
12: Fizz
13: 13
14: 14
15: FizzBuzz
16: 16
17: 17
18: Fizz
19: 19
20: Buzz

Word counts:
  go: 1
  is: 1
  a: 1
  simple: 1
  and: 1
  powerful: 1
  language: 1

Prime numbers (2-30):
2 3 5 7 11 13 17 19 23 29

Type descriptions:
  int: 42
  string: "hello"
  float64: 3.14
  bool: true
  nil value

Guessing game (secret: 42):
  Attempt 1: guessed 25 — Too low!
  Attempt 2: guessed 50 — Too high!
  Attempt 3: guessed 42 — Correct!

Common Mistakes

1. No parentheses around conditions.

// Wrong — Go does not use parentheses
if (x > 0) {
    // ...
}

// Correct
if x > 0 {
    // ...
}

Go does not need (or allow) parentheses around if, for, or switch conditions.

2. Opening brace must be on the same line.

// Wrong — Go requires { on the same line
if x > 0
{
    // ...
}

// Correct
if x > 0 {
    // ...
}

This is enforced by Go’s formatter. The { must always be on the same line as the if, for, switch, or func keyword.

3. Forgetting that switch cases don’t fall through.

// In Go, this only prints "One" — no fall-through by default
switch num {
case 1:
    fmt.Println("One")
case 2:
    fmt.Println("Two")
}

If you come from C or Java, remember that Go’s switch stops after the first matching case. Use fallthrough if you need the old behavior (but you rarely do).

Source Code

You can find the complete source code for this tutorial on GitHub:

GO-5 Source Code on GitHub

What’s Next?

In the next tutorial, Go Tutorial #6: Arrays, Slices, and Maps, you will learn:

  • Arrays — fixed-size collections
  • Slices — dynamic arrays (the most used data structure in Go)
  • Maps — key-value pairs
  • make, append, len, cap
  • range over slices and maps

This is part 5 of the Go Tutorial series. Follow along to learn Go from scratch.