Skip to content
ioob.dev
Go back

Go Basics Part 2 — Conditionals and Loops

· 3 min read
Go Series (2/12)
  1. Go Basics Part 1 — Variables, Constants, and Types
  2. Go Basics Part 2 — Conditionals and Loops
  3. Go Basics Part 3 — Functions
  4. Go Basics Part 4 — Error Handling
  5. Go Basics Part 5 — Arrays, Slices, and Maps
  6. Go Basics Part 6 — Structs and Methods
  7. Go Part 7 — Interfaces
  8. Go Part 8 — Pointers
  9. Go Part 9 — Goroutines and Channels
  10. Go Part 10 — Concurrency Patterns
  11. Go Part 11 — Packages and Modules
  12. Go Part 12 — Generics and Practical Patterns
Table of contents

Table of contents

if — With an Initializer Statement

Go’s if isn’t drastically different from other languages. However, it has one unique feature: you can add an initializer statement before the condition.

package main

import (
    "fmt"
    "strconv"
)

func main() {
    // Standard if
    x := 10
    if x > 5 {
        fmt.Println("x is greater than 5")
    }

    // if with initializer statement
    if n, err := strconv.Atoi("42"); err == nil {
        fmt.Println("conversion succeeded:", n)
    } else {
        fmt.Println("conversion failed:", err)
    }
    // n and err are not accessible here
}

if n, err := strconv.Atoi("42"); err == nil — the part before the semicolon is the initializer, and the part after is the condition. Variables declared this way are only available within the if-else block and disappear outside of it.

Why is this pattern useful? Go has many functions that return errors. Being able to bundle the result check and handling into one block keeps the scope clean. This pattern appears constantly in real-world code, so it’s worth getting familiar with.

Looking at the execution flow of an if with an initializer statement, the scope boundaries become clear.

flowchart LR
    Start([if entry]) --> Init["Execute initializer<br/>n, err := strconv.Atoi(...)"]
    Init --> Cond{"err == nil ?"}
    Cond -->|true| Then["then block<br/>n accessible"]
    Cond -->|false| Else["else block<br/>err accessible"]
    Then --> End([if exit — n, err gone])
    Else --> End

Note that Go doesn’t use parentheses around if conditions. You can write them, but gofmt will automatically strip them out.

switch — No Fallthrough

Most developers working with C-family languages have experienced bugs from forgetting break in a switch statement. Go solves this problem at its root: each case automatically breaks.

package main

import "fmt"

func main() {
    day := "Tue"

    switch day {
    case "Mon":
        fmt.Println("Start of the week")
    case "Tue", "Wed", "Thu":
        fmt.Println("Midweek")
    case "Fri":
        fmt.Println("TGIF!")
    default:
        fmt.Println("Weekend")
    }
}

You can list multiple values in a single case separated by commas. Since there’s no need for break, the code is concise and there’s less room for mistakes.

If you do want C-like fallthrough to the next case, you can explicitly write fallthrough. But in practice, using fallthrough is extremely rare.

Go’s switch has an even more powerful feature: the conditionless switch.

package main

import "fmt"

func main() {
    score := 85

    switch {
    case score >= 90:
        fmt.Println("A")
    case score >= 80:
        fmt.Println("B")
    case score >= 70:
        fmt.Println("C")
    default:
        fmt.Println("F")
    }
}

When you omit the value after switch, each case is evaluated as an independent condition. This is often more readable than a long chain of if-else if-else if. This pattern particularly shines when you have three or more conditions in a row.

for — Go’s Only Loop

Go has no while and no do-while. for is the only loop. Instead, for transforms into several different forms.

package main

import "fmt"

func main() {
    // 1. Traditional for
    for i := 0; i < 5; i++ {
        fmt.Print(i, " ")
    }
    fmt.Println()
    // 0 1 2 3 4

    // 2. Using it like while
    count := 0
    for count < 3 {
        fmt.Print(count, " ")
        count++
    }
    fmt.Println()
    // 0 1 2

    // 3. Infinite loop
    n := 0
    for {
        if n >= 3 {
            break
        }
        fmt.Print(n, " ")
        n++
    }
    fmt.Println()
    // 0 1 2
}

The third form, for {}, is an infinite loop. It’s commonly used for server main loops or event processing. It serves the same role as while(true) in other languages, and in Go, this is the idiomatic way to express it.

When iterating over collections, use range.

package main

import "fmt"

func main() {
    fruits := []string{"apple", "banana", "cherry"}

    for i, fruit := range fruits {
        fmt.Printf("%d: %s\n", i, fruit)
    }

    // If you don't need the index, discard it with _
    for _, fruit := range fruits {
        fmt.Println(fruit)
    }
}

range returns both the index and the value simultaneously. Ignoring the index with _ (the blank identifier) is an extremely common pattern in Go. Since declaring a variable and not using it causes a compile error, without _ you’d be forced to use unnecessary variables.

break, continue, and Labels

break and continue work the same as in other languages. They exit the loop or skip to the next iteration.

package main

import "fmt"

func main() {
    for i := 0; i < 10; i++ {
        if i == 3 {
            continue  // Skip 3
        }
        if i == 7 {
            break     // Stop at 7
        }
        fmt.Print(i, " ")
    }
    fmt.Println()
    // 0 1 2 4 5 6
}

When you want to control an outer loop from within a nested loop, use a label.

package main

import "fmt"

func main() {
outer:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if i == 1 && j == 1 {
                break outer  // Break out of the outer loop at once
            }
            fmt.Printf("(%d, %d) ", i, j)
        }
    }
    fmt.Println()
    // (0, 0) (0, 1) (0, 2) (1, 0)
}

outer: is the label, and break outer breaks out of the loop that label is attached to. Java has a similar feature, but few people know about it. In Go, this pattern is used naturally when breaking out of nested loops is needed.

Labels can also be used with continue.

package main

import "fmt"

func main() {
outer:
    for i := 0; i < 3; i++ {
        for j := 0; j < 3; j++ {
            if j == 1 {
                continue outer  // Skip to the next iteration of the outer loop
            }
            fmt.Printf("(%d, %d) ", i, j)
        }
    }
    fmt.Println()
    // (0, 0) (1, 0) (2, 0)
}

Overusing labels makes code complex, so use them only when truly needed. In most cases, extracting the nested loop into a separate function is cleaner.


The next part covers Go functions. We’ll explore multiple return values, closures, first-class functions, and other features unique to Go functions.

-> Part 3: Functions


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Go Basics Part 1 — Variables, Constants, and Types
Next Post
Go Basics Part 3 — Functions