Control Flow Beginner¶
Go's control flow is minimal by design: one loop keyword (for), no parentheses around conditions, and switches that don't fall through by default. Fewer constructs means fewer bugs.
if / else¶
No parentheses around conditions. Braces are always required.
if x > 0 {
fmt.Println("positive")
} else if x < 0 {
fmt.Println("negative")
} else {
fmt.Println("zero")
}
if with Init Statement¶
Declare a variable scoped to the if/else block. This is extremely common for error handling.
if err := doSomething(); err != nil {
log.Fatal(err)
}
// err is NOT accessible here
if val, ok := cache[key]; ok {
return val
}
Interview Tip
"Go's if with init statement is idiomatic for the comma-ok pattern and error checks. The variable is scoped to the if/else chain, keeping the outer scope clean. You'll see if err := f(); err != nil in virtually every Go codebase."
switch¶
Expression Switch¶
No break needed -- Go switches do not fall through by default.
switch day {
case "Monday":
fmt.Println("Start of week")
case "Friday":
fmt.Println("Almost weekend")
case "Saturday", "Sunday": // multiple values in one case
fmt.Println("Weekend")
default:
fmt.Println("Midweek")
}
Switch with Init Statement¶
switch os := runtime.GOOS; os {
case "linux":
fmt.Println("Linux")
case "darwin":
fmt.Println("macOS")
default:
fmt.Printf("Other: %s\n", os)
}
Tagless Switch (replaces if/else chains)¶
switch {
case score >= 90:
grade = "A"
case score >= 80:
grade = "B"
case score >= 70:
grade = "C"
default:
grade = "F"
}
fallthrough¶
Forces execution to continue into the next case (unconditionally). Rarely used.
switch n := 3; {
case n > 0:
fmt.Println("positive") // printed
fallthrough
case n > 10:
fmt.Println("big?") // also printed (unconditional fallthrough)
}
// Output: positive
// big?
fallthrough is unconditional
Unlike C's fall-through, Go's fallthrough does not re-evaluate the next case condition. It always enters the next case body. This catches people off guard.
Type Switch¶
Determine the dynamic type of an interface value. Covered in depth in the intermediate section on type assertions.
func describe(i interface{}) string {
switch v := i.(type) {
case int:
return fmt.Sprintf("int: %d", v)
case string:
return fmt.Sprintf("string: %q", v)
case bool:
return fmt.Sprintf("bool: %t", v)
default:
return fmt.Sprintf("unknown: %T", v)
}
}
for Loop¶
Go has only for -- no while, no do-while. The for keyword covers all loop patterns.
C-Style (Three-Component)¶
While-Style (Condition Only)¶
Infinite Loop¶
range Over Collections¶
range iterates over slices, arrays, maps, strings, channels, and integers (Go 1.22+).
// Slice / Array -- index and value
nums := []int{10, 20, 30}
for i, v := range nums {
fmt.Printf("index=%d value=%d\n", i, v)
}
// Index only
for i := range nums {
fmt.Println(i)
}
// Value only (discard index)
for _, v := range nums {
fmt.Println(v)
}
// Map -- key and value (iteration order is random)
config := map[string]string{"host": "localhost", "port": "8080"}
for k, v := range config {
fmt.Printf("%s=%s\n", k, v)
}
// Key only
for k := range config {
fmt.Println(k)
}
// String -- index and rune (NOT byte)
for i, r := range "Go语言" {
fmt.Printf("byte_offset=%d rune=%c\n", i, r)
}
// byte_offset=0 rune=G
// byte_offset=1 rune=o
// byte_offset=2 rune=语
// byte_offset=5 rune=言
// Channel -- receives until channel is closed
ch := make(chan int)
go func() {
for _, v := range []int{1, 2, 3} {
ch <- v
}
close(ch)
}()
for v := range ch {
fmt.Println(v)
}
Range Reference Table¶
| Type | First Value | Second Value | Notes |
|---|---|---|---|
[]T, [N]T |
index int |
element T |
Copy of element |
map[K]V |
key K |
value V |
Random order |
string |
byte offset int |
rune rune |
Decodes UTF-8 |
chan T |
element T |
-- | Blocks until closed |
int (Go 1.22+) |
int |
-- | 0 to n-1 |
break, continue, and Labels¶
Basic break and continue¶
for i := 0; i < 10; i++ {
if i == 5 {
break // exit the loop entirely
}
if i%2 == 0 {
continue // skip to next iteration
}
fmt.Println(i) // 1, 3
}
Labeled Statements¶
Labels let you break or continue an outer loop from inside a nested loop.
outer:
for i := 0; i < 3; i++ {
for j := 0; j < 3; j++ {
if i == 1 && j == 1 {
break outer // breaks the outer loop, not just inner
}
fmt.Printf("(%d,%d) ", i, j)
}
}
// Output: (0,0) (0,1) (0,2) (1,0)
// Label with continue
rows:
for _, row := range matrix {
for _, val := range row {
if val < 0 {
continue rows // skip entire row
}
}
processRow(row)
}
Interview Tip
"Labeled break/continue is the idiomatic way to exit nested loops in Go. It's cleaner than boolean flags and more controlled than goto. It's commonly used in parser and matrix-processing code."
goto¶
Go supports goto but with restrictions: you cannot jump over variable declarations or into other blocks.
Avoid goto in almost all cases
goto exists in Go primarily for machine-generated code and very specific cleanup patterns. In practice, labeled break/continue or early return handles every case more clearly. Using goto in interview code signals poor design judgment.
Quick Reference¶
| Construct | Syntax | Go Difference |
|---|---|---|
if with init |
if x := f(); x > 0 { } |
Init var scoped to if/else |
switch |
switch x { case 1: ... } |
No fall-through by default |
| Tagless switch | switch { case x > 0: ... } |
Replaces if/else chains |
| Type switch | switch v := i.(type) { } |
Runtime type checking |
| C-style for | for i := 0; i < n; i++ { } |
Only loop keyword |
| While-style | for condition { } |
No while keyword |
| Infinite loop | for { } |
Common for servers |
| Range (slice) | for i, v := range s { } |
Copies elements |
| Range (map) | for k, v := range m { } |
Random order |
| Range (string) | for i, r := range s { } |
Decodes runes |
| Range (int) | for i := range n { } |
Go 1.22+ |
| Labeled break | outer: for ... { break outer } |
Exit outer loop |
fallthrough |
case 1: fallthrough |
Unconditional |
Best Practices¶
- Use
ifinit statements for error handling --if err := f(); err != nil { }keeps scope tight. - Prefer tagless
switchover longif/elsechains -- it's cleaner and more readable. - Always handle the
defaultcase in switches that process external input. - Use labeled
breakfor nested loops instead of boolean flags orgoto. - Use
for rangeinstead of manual indexing when you don't need to modify the index. - Prefer early returns over deep nesting -- flatten your control flow.
Common Pitfalls¶
Range loop variable reuse (pre-Go 1.22)
Before Go 1.22, the loop variable was reused across iterations. Capturing it in a goroutine without copying was a classic bug.
Go 1.22+ fixes this by creating a new variable per iteration by default.Range copies elements
range gives you a copy of each element, not a reference.
Map iteration order is random
m := map[string]int{"a": 1, "b": 2, "c": 3}
for k := range m {
fmt.Print(k) // different order each run
}
slices.Sorted(maps.Keys(m)).
fallthrough skips condition checks
Performance Considerations¶
rangevs index loop: Performance is identical for slices of basic types. For large structs,rangecopies each element -- use index-based access or a slice of pointers to avoid copies.switchvsif/else: The compiler optimizes both similarly.switchis preferred for readability, not performance.- Map range allocation: Iterating over a map allocates internally. For hot paths, consider pre-extracting keys into a slice.
- Break early: In search loops,
breakon first match. Don't scan the entire collection when you have the answer.
Key Takeaways¶
- Go has only
forfor loops -- it covers C-style, while-style, infinite, and range patterns. switchdoes not fall through by default -- nobreakneeded (opposite of C/Java).ifandswitchsupport init statements for scoped variable declarations.rangeover strings yields runes (Unicode code points), not bytes.rangecopies elements -- use index-based access to modify slice elements in place.- Go 1.22+ supports
rangeover integers and fixes the loop variable capture bug. - Use labeled
break/continuefor nested loops; avoidgoto.