Control Flow
Learn how Go programs make decisions with if statements, switch cases, and loops. Master Go's approach to control structures and understand why Go only has one loop keyword.
Programs need to make decisions. Read a configuration file and behave differently based on what's inside. Check if a user has permission before processing their request. Retry a network call if it fails. Execute different code paths depending on input.
Go provides two decision-making constructs: if statements and switch statements. That's it. No ternary operator, no complex pattern matching, no unless keyword. This minimalism is deliberate—fewer constructs mean more consistent code.
If Statements
The basic if statement evaluates a condition and executes a block:
package main
import "fmt"
func main() {
temperature := 75
if temperature > 70 {
fmt.Println("It's warm outside")
}
}
The condition must be a boolean expression. Go doesn't treat numbers or strings as truthy or falsy like Python or JavaScript does:
x := 1
if x { // Compile error: non-bool x used as if condition
// ...
}
You must write the comparison explicitly:
if x != 0 {
// ...
}
This prevents bugs where you meant to check equality but used assignment:
// In C, this compiles and always executes the block:
// if (x = 0) { ... }
// In Go, this is a compile error:
if x = 0 { // Error: assignment used as value
// ...
}
Notice the absence of parentheses around the condition. Go doesn't require them:
if temperature > 70 { // idiomatic
// ...
}
if (temperature > 70) { // works but unnecessary
// ...
}
This requirement eliminates Apple's famous "goto fail" SSL bug, where indentation suggested a statement was inside an if but it wasn't. In Go, the braces make scope unambiguous.
Else Clauses
Add an else block for the alternate path:
if temperature > 70 {
fmt.Println("It's warm outside")
} else {
fmt.Println("It's cool outside")
}
Chain conditions with else if:
if temperature > 85 {
fmt.Println("It's hot")
} else if temperature > 70 {
fmt.Println("It's warm")
} else if temperature > 50 {
fmt.Println("It's cool")
} else {
fmt.Println("It's cold")
}
Go evaluates conditions top to bottom and executes the first matching block. Once a condition matches, the rest are skipped.
Short Statement Form
You can execute a statement before the condition:
if err := connectToDatabase(); err != nil {
fmt.Println("Connection failed:", err)
return
}
// err is not in scope here
This form is common for checking errors immediately. The variable err exists only within the if and else blocks, not outside. This scoping prevents variables from leaking into broader scope where they're not needed.
Another example with computation:
if value := calculate(); value > threshold {
fmt.Println("Value exceeds threshold:", value)
} else {
fmt.Println("Value within limits:", value)
}
// value is not in scope here
The short statement form reduces clutter and makes the relationship between computation and condition clear. You see the variable created, used, and destroyed in one place.
Switch Statements
Switch statements handle multiple conditions elegantly. Here's a switch on a value:
day := "Tuesday"
switch day {
case "Monday":
fmt.Println("Start of the week")
case "Tuesday", "Wednesday", "Thursday":
fmt.Println("Midweek")
case "Friday":
fmt.Println("Almost weekend")
case "Saturday", "Sunday":
fmt.Println("Weekend!")
default:
fmt.Println("Unknown day")
}
Each case can match multiple values separated by commas. The first matching case executes, then the switch exits. Unlike C or Java, Go doesn't fall through to the next case—you don't need break statements:
x := 2
switch x {
case 1:
fmt.Println("one")
case 2:
fmt.Println("two") // Prints "two" and exits
case 3:
fmt.Println("three")
}
If you want fall-through behavior (rare), use the fallthrough keyword explicitly:
x := 1
switch x {
case 1:
fmt.Println("one")
fallthrough
case 2:
fmt.Println("also two") // Also prints
}
// Output:
// one
// also two
Fallthrough is uncommon in Go code. The default no-fallthrough behavior prevents bugs where you forget a break.
Expression Switches
Switch statements don't require a value to switch on. You can use boolean conditions:
temperature := 75
switch {
case temperature > 85:
fmt.Println("hot")
case temperature > 70:
fmt.Println("warm")
case temperature > 50:
fmt.Println("cool")
default:
fmt.Println("cold")
}
This is cleaner than a chain of else if statements. Each case evaluates a boolean expression, and the first true case executes.
You can mix value comparisons, function calls, and complex conditions:
switch {
case x < 0:
fmt.Println("negative")
case x == 0:
fmt.Println("zero")
case isPrime(x):
fmt.Println("prime")
case isEven(x):
fmt.Println("even")
default:
fmt.Println("odd composite")
}
The cases evaluate in order. Once a case matches, evaluation stops.
Switch with Short Statement
Like if, switch supports a short statement:
switch err := processRequest(); {
case err == nil:
fmt.Println("Success")
case errors.Is(err, ErrNotFound):
fmt.Println("Not found")
default:
fmt.Println("Error:", err)
}
The variable err exists only within the switch statement. This pattern is common for error handling where you want to branch on different error types.
Type Switches
Switch statements can determine the concrete type of an interface value:
func describe(i interface{}) {
switch v := i.(type) {
case int:
fmt.Println("Integer:", v)
case string:
fmt.Println("String:", v)
case bool:
fmt.Println("Boolean:", v)
default:
fmt.Println("Unknown type")
}
}
The i.(type) syntax only works in switch statements. It extracts the dynamic type of the interface and assigns it to v with the correct type in each case. Inside the case int block, v has type int, not interface{}.
Type switches matter when working with interfaces that could hold different concrete types. We'll explore interfaces deeply in a later article, but the syntax appears here because it's part of the switch statement.
Early Returns
Go code often uses early returns rather than nested if statements:
func processUser(id int) error {
if id <= 0 {
return errors.New("invalid ID")
}
user, err := fetchUser(id)
if err != nil {
return err
}
if !user.Active {
return errors.New("user not active")
}
// Main logic here with minimal nesting
return updateUser(user)
}
This style keeps the happy path unindented. Error conditions return immediately. The alternative would be deeply nested:
func processUser(id int) error {
if id > 0 {
user, err := fetchUser(id)
if err == nil {
if user.Active {
// Main logic here, deeply indented
return updateUser(user)
} else {
return errors.New("user not active")
}
} else {
return err
}
} else {
return errors.New("invalid ID")
}
}
The early return version is easier to read. Each error check returns immediately, and the main logic stays at the top level. This pattern appears throughout Go code.
No Ternary Operator
Go doesn't have a ternary operator. In C or Java you might write:
// C/Java
int max = (a > b) ? a : b;
In Go, use an if statement:
var max int
if a > b {
max = a
} else {
max = b
}
This looks verbose, but ternary operators often lead to unreadable code when nested. Go's designers chose to keep one way to write conditionals rather than adding syntax for brevity.
For simple cases, you can extract to a function:
func max(a, b int) int {
if a > b {
return a
}
return b
}
result := max(a, b)
The function is reusable and testable. This approach scales better than inline ternary expressions.
Comparing to Other Languages
Python has truthiness—empty strings, zero, and empty collections are false. Go requires explicit boolean expressions. This prevents bugs where you check for existence but accidentally treat zero as absent.
JavaScript has both == and === for equality. Go has only ==, which compares values without type coercion. Type safety eliminates the need for multiple equality operators.
C requires parentheses around conditions and doesn't require braces for single statements. This led to Apple's goto fail bug. Go's required braces prevent that entire class of errors.
Java has switch fall-through by default. Go prevents fall-through unless you explicitly use fallthrough. This eliminates bugs from forgotten break statements.
When to Use If vs Switch
Use if when:
- You have one or two conditions
- The condition is complex
- You're checking error values
Use switch when:
- You have three or more conditions
- You're comparing a value against multiple constants
- You're doing type assertions
Switch statements are particularly clean for mapping values:
switch httpStatus {
case 200:
return "OK"
case 404:
return "Not Found"
case 500:
return "Internal Server Error"
default:
return "Unknown"
}
This is clearer than a chain of else if statements.
What's Next
Control flow determines what code runs and when. The next piece is repetition—loops that execute code multiple times. Go unifies all looping constructs into a single for statement that handles everything from counting to iterating collections.
Once you can make decisions and repeat operations, you'll have the tools to process data structures. We'll explore arrays, slices, and maps—Go's collection types that let you organize and manipulate groups of values efficiently.
The pattern holds: Go provides minimal syntax that covers necessary cases. No ternary operator, no do-while loops, no unless keyword. Fewer constructs mean more consistent code and less to remember.
Ready to explore loops and iteration?
