Go Conditional Logic & Loops: if, switch, for Guide

Go Conditional Logic & Loops: Complete Guide
A program without logic is just a list of instructions. To build real-world applications, you need your code to make decisions, handle errors, and process collections of data iteratively.
Go's approach to control flow is remarkably minimalist. There are no while or do-while loops. There are no parentheses around conditions. This simplicity makes Go code incredibly fast to read and difficult to hide bugs in.
In most languages, you write if (condition) { ... }. In Go, the parentheses are gone: if condition { ... }. This small change makes the code feel much cleaner and reduces syntactic noise.
1. The Execution Mirror: Branch Prediction Physics
Every time your Go program encounters an if statement, the CPU must make a guess about which path the code will take. This is known as Branch Prediction.
The Flow Physics
- The Guess: Modern CPUs try to "look ahead" and begin executing the code inside an
ifblock before the condition is even evaluated. - The Reallocation Policy: If the CPU guesses wrong (a "Misprediction"), it must flush its entire pipeline (the "Execution Mirror") and start over from the correct branch. This can waste 10-20 CPU cycles.
- The Go Optimization: Go's minimalist syntax and "Happy Path" coding style (where the error cases return early) help the CPU's branch predictor remain highly accurate, ensuring maximum pipeline efficiency.
2. If / Else Statements
The if statement is the most fundamental building block of logic. In Go, an if statement can also include an optional initialization statement before the condition. This is often used for error handling or scoping a variable tightly to only the if block.
func main() {
score := 85
// Basic if/else
if score >= 90 {
fmt.Println("A+")
} else if score >= 80 {
fmt.Println("B")
} else {
fmt.Println("C")
}
// If with initialization
if age := 18; age >= 18 {
fmt.Println("You can vote!")
}
// 'age' is not visible here outside the if/else block
}The Switch Statement
Go's switch is a more powerful and safer version than what you find in C or Java. By default, Go automatically breaks at the end of each case, meaning you Don't need to write break after every block.
If you do want to fall through to the next case, you must explicitly use the fallthrough keyword.
func main() {
day := "Monday"
switch day {
case "Monday", "Tuesday", "Wednesday", "Thursday", "Friday":
fmt.Println("It's a weekday.")
case "Saturday", "Sunday":
fmt.Println("It's the weekend!")
default:
fmt.Println("Invalid day.")
}
}4. The Loop Mirror: Jump Physics and Unrolling
Go's decision to have only one loop—the for loop—is not just about simplicity; it is about Predictable Compilation.
The Iteration Physics
- Jump Tables: At the assembly level, a
forloop is just a "Compare" instruction followed by a "Jump" instruction. - Loop Unrolling: In high-performance hot-paths, the Go compiler can "unroll" a loop (copy-pasting the body 4 or 8 times) to reduce the number of "Jumps" the CPU has to make, literally trading binary size for speed.
- The Infinite Loop:
for {}is the ultimate "Spin Mirror." It tells the OS: "Give me this CPU core until I explicitly yield it."
5. The Only Loop: The for Loop
In Go, there is only one looping keyword: for. However, it is extremely flexible and can be used to replicate the behavior of while and do-while loops from other languages.
The Three Faces of for
Looping Over Collections
To iterate over arrays, slices, or maps, Go provides the range keyword. It returns both the index and the value of each element.
func main() {
users := []string{"Alice", "Bob", "Charlie"}
for i, name := range users {
fmt.Printf("User #%d: %s\n", i, name)
}
// If you don't need the index, use the blank identifier (_)
for _, name := range users {
fmt.Println("Name:", name)
}
}Looping Over Maps
Ranging over a map follows the same syntax as slices, but the iteration order is not guaranteed — Go deliberately randomises map iteration to prevent developers from relying on an undefined order:
func main() {
scores := map[string]int{
"Alice": 95,
"Bob": 87,
"Carol": 92,
}
for name, score := range scores {
fmt.Printf("%s scored %d\n", name, score)
}
}If you need to process map entries in a consistent order, extract the keys into a slice, sort it, then iterate:
import "sort"
names := make([]string, 0, len(scores))
for name := range scores {
names = append(names, name)
}
sort.Strings(names)
for _, name := range names {
fmt.Printf("%s: %d\n", name, scores[name])
}Break, Continue, and Goto
Go provides three keywords for controlling loop flow:
break
Immediately exits the innermost for or switch block.
for i := 0; i < 10; i++ {
if i == 5 {
break // Stop at 5
}
fmt.Println(i)
}continue
Skips the rest of the current iteration and moves to the next one.
for i := 0; i < 10; i++ {
if i%2 == 0 {
continue // Skip even numbers
}
fmt.Println(i) // Prints 1, 3, 5, 7, 9
}Labeled Breaks for Nested Loops
When you need to break out of multiple nested loops at once, Go supports labeled statements:
outer:
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
if i+j == 6 {
break outer // Exits both loops
}
}
}Type Switch: Go's Powerful Pattern
Go's switch has a special form called a type switch that evaluates the dynamic type of an interface value. This is commonly used when a function accepts an interface{} or any parameter:
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Integer: %d\n", v)
case string:
fmt.Printf("String: %q\n", v)
case bool:
fmt.Printf("Boolean: %t\n", v)
default:
fmt.Printf("Unknown type: %T\n", v)
}
}Type switches are extensively used in JSON decoders, middleware, and any code that processes dynamic data structures.
E-E-A-T Practical Tip: Initialisation in If Statements
One of Go's most productive patterns is the if err := doSomething(); err != nil form. By combining the function call and error check into a single if statement, you scope the error variable tightly and eliminate the need for a separate variable declaration:
// Verbose version
result, err := queryDatabase()
if err != nil {
return err
}
// Idiomatic Go version (scopes result and err to the if block)
if result, err := queryDatabase(); err != nil {
return fmt.Errorf("queryDatabase: %w", err)
}This is the pattern you will see in virtually every production Go codebase for error handling in functions that return multiple values.
Further Reading
For related Go fundamentals, see Go variables, types, and constants and Go functions, pointers, and structs. To apply these control flow patterns to collections, see Go slices, maps, and collections.
Phase 5: Execution Flow Mastery Checklist
- Verify "Happy Path" Alignment: Ensure that your main logic flows vertically down the page, with error cases handled early via
if err != nil { return err }. - Audit Branch Complexity: Identify
ifstatements with more than 3 nested levels and refactor them into aswitchor separate functions to help the CPU Branch Predictor. - Implement Labeled Breaks: For complex search algorithms, use labeled breaks to exit nested loops cleanly without using boolean "Found" flags.
- Test Switch Fallthrough: Confirm that none of your
switchcases are missing a break accidentally (Go handles this implicitly, but clarify your intent with a comment). - Use Small-Scope Init: Always use the
if v, err := func(); err != nilpattern to keep variable leakage to a minimum.
Read next: Go Functions, Pointers, and Structs: The Memory-Layout Mirror →
Next Steps
Now that you can control the flow of your program and process data in loops, we need to talk about Structure. In the next module, we will explore how to group your code into reusable Functions and how to define custom complex data types using Structs.
Common Mistakes with Go Control Flow
1. Missing break in switch — not needed
Unlike C and Java, Go switch cases do not fall through by default. Each case breaks automatically. Use fallthrough explicitly if you want fall-through behaviour. This eliminates a whole class of bugs.
2. Using for with no condition as an infinite loop
for {} is Go's infinite loop — there is no while keyword. This is intentional. Add a break or return inside the loop body to exit, or use for condition {} with a boolean expression.
3. Shadowing the loop variable in goroutines
for _, v := range items {
go func() { fmt.Println(v) }() // all goroutines print the last value
}Capture v explicitly: go func(v Item) { fmt.Println(v) }(v). The Go FAQ on loop variable capture explains this classic pitfall.
4. Using if err != nil without returning
Checking an error but continuing execution as if it did not occur is a logic bug. Every if err != nil block should either return, log and return, or explicitly handle the error before continuing.
5. Overly complex switch with many cases that should be a map
A switch with 20+ string cases mapping to fixed values is better expressed as a map[string]T lookup — it is faster, more readable, and easier to extend.
Frequently Asked Questions
Why does Go only have one loop keyword?
Go's designers deliberately kept the language small. A single for keyword covers while, do-while, and traditional for patterns with different forms of the same construct. See the Go spec on for statements.
Can I use break with a label in Go?
Yes. break Label exits the labelled outer loop or switch, which is useful for breaking out of nested loops without a boolean flag. continue Label similarly skips to the next iteration of the labelled loop.
What is the idiomatic way to iterate over a string in Go?
for i, r := range s iterates over Unicode code points (runes), not bytes. If you need byte-level iteration, use for i := 0; i < len(s); i++. The Go blog on strings covers the distinction between bytes and runes.
