Branches and Loops

Branches

In Go, there are several methods for implementing branches to control the flow of program execution.

if

The most common method is the if statement, which evaluates a condition and executes a block of code if that condition is true

if condition {
    // code, runs only if the condition is true
}

Any condition that ultimately evaluates to a boolean value can be used in a condition:

if 2 > 1 {
    fmt.Println("Hello Follow The Pattern!")
}

if ... else

This extends the previous case by allowing you to specify code in the else branch that will run only if the condition fails. The syntax is:

if condition {
    // code, runs only if the condition is true
} else {
    // code, runs only if the condition is false
}

In the following example, the value pointer is checked; if it's not nil, the value is printed, otherwise, the program terminates with an error:

var value *int
 
if value != nil {
    fmt.Println(*value)
} else {
    log.Fatal("value is empty")
}

if ... else if ... else

When multiple true branches are necessary in the condition, this form is one of the choices for handling it. The syntax looks like this:

if condition1 {
    // code, runs only if the first condition is true
} else if condition2 {
    // code, runs only if the second condition is true
} else {
    // code, runs only if none of the conditions are true
}

Here is an example of using multiple branches. This piece of code notifies the user if their balance is zero or below:

balance := 0
 
if balance < 0 {
    fmt.Println("Balance is below 0, add funds now or you will be charged a penalty.")
} else if balance == 0 {
    fmt.Println("Balance is equal to 0, add funds soon.")
} else {
    fmt.Println("Your balance is greater than 0.")
}

if with assignment

The combination of assignment and if can be simplified using this syntax element, which binds the variable's reference to the block.

if value, ok := os.LookupEnv("LOG_LEVEL"); ok {
    // value and ok are declared and attached to this condition
} else if value == "DEBUG" {
    // and to all the branches
} else {
    fmt.Printf("LOG_LEVEL has an unexpected value: \"%s\"", value)
}

switch ... case

This language element is also used for handling multiple logical branches, resulting in more readable and concise code. The syntax:

today := time.Now()
 
switch expression {
case value1:
    // code, runs only if the value is equal to the expression
case value2:
    // code, runs only if the value is equal to the expression
...
...
default:
    // code, runs only if none of the above is equal to the expression, this branch is not mandatory
}

The following example shows a piece of code that outputs the name of the day if it's a weekday. If no match is found, the default branch will run, indicating it's the weekend:

today := time.Now()
 
switch today.Weekday() {
case time.Monday:
    fmt.Println("Monday")
case time.Tuesday:
    fmt.Println("Tuesday")
case time.Wednesday:
    fmt.Println("Wednesday")
case time.Thursday:
    fmt.Println("Thursday")
case time.Friday:
    fmt.Println("Friday")
default:
    fmt.Println("Weekend!")
}

If no default branch is provided and none of the conditions are true, execution will exit the switch code block.

today := time.Now()
 
switch today.Weekday() {
case time.Monday:
    fmt.Println("Monday")
case time.Tuesday:
    fmt.Println("Tuesday")
}

switch ... case with assignment

Similar to the if case, you can use abbreviated assignment in conjunction with the switch block.

switch today := time.Now().Weekday(); today {
case time.Monday:
    fmt.Println("Monday")
case time.Tuesday:
    fmt.Println("Tuesday")
case time.Wednesday:
    fmt.Println("Wednesday")
case time.Thursday:
    fmt.Println("Thursday")
case time.Friday:
    fmt.Println("Friday")
default:
    fmt.Println("Weekend!")
}

This language feature can save you few line of code. The variable created within the switch block cannot be used outside of that block.

switch ... case without condition

It's not mandatory to define a condition when creating a switch block. In this case, the conditions following the case keyword will be evaluated based on whether they're true or false, determining which branch the assignment will continue on.

balance := 0
 
switch {
case balance < 0:
    fmt.Println("Balance is below 0, add funds now or you will be charged a penalty.")
case balance == 0:
    fmt.Println("Balance is equal to 0, add funds soon.")
default:
    fmt.Println("Your balance is greater than 0.")
}

Loops

Loops are language features that allow a code block to be executed multiple times, preventing unnecessary code duplication. Several types of loops are available in Go:

  • Three-component loop
  • while-like loop
  • Infinite loop
  • For-each loop

Three-component loop

This is the classic implementation of a loop, consisting of three parts. The first part is a statement that is executed only once before the loop. The second part is a condition that controls when the looping will stop. The third part is also a statement that is executed after each iteration. Syntax:

for first statement; condition; post statement {
    // code, runs until the condition is true
}

A short example:

for i := 0; i < 10; i++ {
    fmt.Println(i)
}

Iterating over a list:

list := []int{0, 1, 2, 3, 4}
for i := 0; i < len(list); i++ {
    fmt.Println(list[i])
}

Using the continue keyword allows you to skip certain iterations of the loop. In the following example, only the values with odd indices are printed:

list := []int{0, 1, 2, 3, 4}
for i := 0; i < len(list); i++ {
    if i%2 == 0 {
        continue
    }
    fmt.Println(list[i])
}

By using the break keyword, you can exit loop before it would finish. The next snippet demonstrates a simple search algorithm that prints whether the searched expression exists in a list:

filter := "apple"
 
list := []string{"banana", "apple", "orange"}
 
found := false
 
for i := 0; i < len(list); i++ {
    if list[i] == filter {
        found = true
        break
    }
}
 
fmt.Println("Does it contain apple?", found)

While-like loop

In other languages, this would be a separate language feature. In Go, we can use the for keyword to condition-based loops as well. Essentially, you omit the first and third statements from the previous example. The syntax is:

for condition {
    // code, runs until the condition is true
}

A simple example:

n := 1
for n < 5 {
    n *= 2
    fmt.Println(n)
}

Infinite loop

This without a condition and statements, meaning it will run infinitely.

for {
    // code, runs infinite times
}

Implementation example:

package main
 
import (
	"fmt"
	"math/rand"
	"time"
)
 
func main() {
	// Seed with current time
	rand.Seed(time.Now().UnixNano())
 
	// Define min and max range
	min := 10
	max := 50
 
	for {
		// Generate a random number in range [min, max]
		randomNumber := rand.Intn(max-min+1) + min
		fmt.Printf("Random number between %d and %d: %d\n", min, max, randomNumber)
		if randomNumber%2 == 0 {
			break
		}
	}
}

To prevent the program from running indefinitely, a condition must be placed within the loop. If the condition is true, the break will exit the loop. Otherwise, the loop would run indefinitely.

For-each loop

This type simplifies iteration over a list, automatically managing indexing for you.

for [INDEX OF THE CURRENT ITEM], [VALUE OF THE CURRENT ITEM] := range [LIST] {
    // code, runs on each item of the list
}
list := []string{"Follow", "The", "Pattern"}
 
for index, value := range list {
    fmt.Println(index, value)
}

Iterators

Traditionally, Go's for...range loop could iterate over arrays, slices, maps, strings, and channels. With Go 1.23, this capability extends to functions that conform to specific signatures, enabling custom iteration behavior. These iterator functions can be defined to produce sequences of values, which the for...range loop can consume.

Defining an Iterator Function

An iterator function in Go 1.23 is a function that takes another function (often called yield) as an argument. This yield function is invoked for each item in the sequence, and it returns a boolean indicating whether to continue the iteration.

Creating a Simple Iterator Function

package main
 
import "fmt"
 
// Iterator function that generates numbers from 0 to n-1
func generate(n int) func(func(int) bool) {
    return func(yield func(int) bool) {
        for i := 0; i < n; i++ {
            if !yield(i) {
                return
            }
        }
    }
}
 
func main() {
    // Using the iterator function with for...range
    for num := range generate(5) {
        fmt.Println(num)
    }
}

Run this in your browser (opens in a new tab)

One of the main benefit of using iterators that alues are generated on-the-fly, which can lead to performance improvements, especially with large datasets.

Handling breaks with iterator function

package main
 
import "fmt"
 
// Iterator function that generates numbers from 0 to n-1
func generate(n int) func(func(int) bool) {
	return func(yield func(int) bool) {
		for i := 0; i < n; i++ {
			if !yield(i) {
				fmt.Println("break")
				return
			}
		}
	}
}
 
func main() {
	// Using the iterator function with for...range
	for num := range generate(5) {
		fmt.Println("num", num)
		if num == 3 {
			break
		}
	}
 
}

Run without installation here (opens in a new tab)