Go Basics #3 Control Flow — if, for, switch

3 min read

In #2 Variables, Types, Constants you saw the tools for handling values. This time — making branches and loops with those values.

if / else if / else #

Conditional branching. Almost identical to other languages, except there are no parentheses.

basic if
score := 85

if score >= 90 {
	fmt.Println("A")
} else if score >= 80 {
	fmt.Println("B")
} else {
	fmt.Println("C")
}

It’s if condition, not if (condition). Coming from C or Java this feels odd at first, but you adjust quickly.

Short statement #

Inside the condition you can declare a variable and immediately use it. A pattern you’ll meet very often in Go.

if with short statement
if v, ok := lookup(key); ok {
	fmt.Println("found:", v)
} else {
	fmt.Println("not found")
}

v, ok := lookup(key) happens inside the if, and v and ok are visible only within the if/else block. They disappear once the block exits.

This pattern shows up everywhere in Go code. It’s also the standard shape for error checking (#4).

Conditions are bool only #

bool only
x := 5

// if x { ... }     ✗ x is an int
if x > 0 { ... }     // OK

// if v := lookup(); v { ... }   ✗ v has to be a bool

There’s no automatic truthy/falsy conversion like JavaScript. Conditions must be explicit.

for — the only loop #

Go has no while. Every loop is expressed with a single for.

1) Classic for — three parts #

classic for
for i := 0; i < 5; i++ {
	fmt.Println(i)
}
// 0, 1, 2, 3, 4

Same shape as C/Java. Init ; condition ; post.

2) Like a while — condition only #

while-style
n := 0
for n < 5 {
	fmt.Println(n)
	n++
}

Of the three parts, only the condition is present. Same meaning as a while in other languages.

3) Infinite loop — no condition #

infinite for
for {
	// ...
	if shouldStop() {
		break
	}
}

for { } is an infinite loop. Same as while (true).

4) for range — iterating collections #

for range over array/slice
fruits := []string{"apple", "banana", "grape"}

for i, fruit := range fruits {
	fmt.Println(i, fruit)
}
// 0 apple
// 1 banana
// 2 grape
for range over map
ages := map[string]int{
	"Curtis": 30,
	"Alice":  25,
}

for name, age := range ages {
	fmt.Println(name, age)
}
for range over string (rune by rune)
for i, r := range "안녕" {
	fmt.Printf("%d: %c\n", i, r)
}
// 0: 안
// 3: 녕  (UTF-8 byte index)

for range is very powerful. Drop the second variable when you only need the index/key, or use _ for the first when you only need the value.

taking only what you need
for i := range fruits {        // index only
	fmt.Println(i)
}

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

break and continue #

Loop flow control.

break / continue
for i := 0; i < 10; i++ {
	if i == 5 {
		break       // exit the loop
	}
	if i%2 == 0 {
		continue    // jump to next iteration
	}
	fmt.Println(i)
}
// 1, 3

Labels for nested loops #

When you want to break out of multiple nested loops at once.

labels
outer:
for i := 0; i < 5; i++ {
	for j := 0; j < 5; j++ {
		if i*j > 6 {
			break outer    // break out to the outer label in one shot
		}
	}
}

Not used often, but clean for deeply nested cases.

switch — Go’s biggest difference #

Go’s switch does not fall through automatically. The opposite of C/Java.

switch — implicit break
day := "mon"

switch day {
case "mon", "tue", "wed", "thu", "fri":
	fmt.Println("weekday")
case "sat", "sun":
	fmt.Println("weekend")
default:
	fmt.Println("?")
}

Each case has an automatic break. You don’t write break like in old C. You can also group multiple values in one case with commas.

Forced fallthrough #

If you really want to fall through to the next case, use the fallthrough keyword.

fallthrough — explicit
n := 1

switch n {
case 1:
	fmt.Println("one")
	fallthrough
case 2:
	fmt.Println("two")
}
// one
// two

Almost never used. Only when the intent is unmistakable.

Conditional switch — short form of if/else #

A switch with no expression turns long if/else chains into something compact.

conditional switch
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")
}

Each case is a bool expression. Often nicer than a long if/else if chain.

switch + short statement #

Like if, switch allows a short statement.

switch with short statement
switch v := getValue(); {
case v > 100:
	fmt.Println("large")
case v > 0:
	fmt.Println("positive")
default:
	fmt.Println("zero or below")
}

Type switch — branching on interface #

We’ll cover this in earnest in Intermediate #1 Interfaces, but switch is also used for type checking.

type switch (preview)
func describe(i interface{}) {
	switch v := i.(type) {
	case int:
		fmt.Println("int:", v)
	case string:
		fmt.Println("string:", v)
	case bool:
		fmt.Println("bool:", v)
	default:
		fmt.Println("unknown")
	}
}

The special i.(type) syntax. It branches on which concrete type the interface holds.

goto — almost never #

Go does have goto, but it’s almost never used. You’ll only see it in very specific cases (escaping nesting, cleanup code).

Short-if pitfall — braces are required #

Unlike JavaScript, Go always requires braces.

braces required
// if condition doSomething()       ✗ compile error
if condition {
	doSomething()
}

Thanks to this design, JavaScript-style “fake-indentation if” pitfalls don’t exist at all.

Common patterns #

1) Error checking #

The if err != nil pattern is one of the shapes you’ll see most often in Go code.

error checking
file, err := os.Open("data.txt")
if err != nil {
	return err
}
defer file.Close()
// ... normal processing

Covered in detail in #4.

2) Key existence check #

check map key
ages := map[string]int{"Curtis": 30}

if age, ok := ages["Curtis"]; ok {
	fmt.Println(age)
}

map[key] can return two values — the value and an ok flag. This short-statement pattern is standard.

3) early return #

A pattern of returning early instead of nesting deep with if/else.

early return
func process(s string) error {
	if s == "" {
		return fmt.Errorf("empty string")
	}
	if len(s) > 100 {
		return fmt.Errorf("string too long")
	}

	// main logic — only one indent level
	return nil
}

The same pattern you saw in JavaScript #4 Functions. Recommended in Go too.

Wrap-up #

What we covered:

  • if condition { } — no parentheses, braces required
  • Short statement — if v, ok := ...; ok { } shows up often
  • Conditions are bool only — no automatic truthy/falsy conversion
  • for is the only loop — no while/do-while
  • Four shapes — classic, condition-only, infinite, range
  • for range over slices/maps/strings (strings are rune)
  • switch has implicit break — fallthrough is explicit
  • Conditional switch cleans up if/else chains
  • i.(type) for type switch (preview)

In the next post (#4 Functions, Multiple Return, error Type) we cover Go’s functions — how they’re defined, multiple return, and most importantly the error-handling pattern.

X