Back

4. Blocks, Shadows and Control Structures


Blocks

Go lets you declare variables in lots of places. You can declare them outside of functions, as the parameters to functions and as local variables within functions.

Shadowing Variables

func main() {
  x := 10
  if x > 5 {
    fmt.Println(x) // 10
    x := 5
    fmt.Println(x) // 5
  }
  fmt.Println(x) // 10
}

A shadowing variable is a variable that has the same name as a variable in a containing block. For as long as the shadowing variable exists, you can’t access the shadowed variable (the parent).

This is why it’s dangerous to use := all the time as it’s very easy to accidentally created shadowing variables

You can also shadow package imports:

func main() {
  x := 10
  fmt.Println(x)
  fmt := "a string"
  fmt.Println(fmt)
}

This will cause an error fmt.Println undefined (type string has no field or method Println).

Detecting Shadowed Variables

Given the subtle bugs that this can introduce, it;’s a good idea to ensure you don’t have any shadowed variables introduced in your programs. Neither go vet nor golanci-lint will detect shadowing, but you can add shadowing detection to your build process by installing the shadow linter on your machine:

go install golang.org/x/tools/go/analysis/passes/shadow/cmd/shadow@latest

If you are building with a Makefile, considering including a shadow in the vet task:

vet:
    go vet ./...
    shadow ./...
.PHONY:vet

When you run make vet against the previous code, you'll see the shadow variable is now detected: declaration of "x" shadow declaration at line 6

The Universe Block

Go is a small language with only 25 keywords, but built-in types (int, string, …), constants (true and false), functions (make or close) aren’t in that list. Neither is nil.

Rather than make them keywords, they are considered predeclared identifiers - they are defined in the universe block, which is the block that contains all blocks. Similarly, they can be shadowed in the other scopes:

fmt.Println(true) // true
true := 10
fmt.Println(true) // 10

Not even ‘shadow’ detects shadowing of universe block identifiers - you should NEVER do this.

Statements

If/Else Statement

It works like most other languages, however there are no parentheses around the condition and we can also define variables within the braces of an if or else statement will only be accessible within that block.

n := rand.Intn(10)
if n == 0 {
	fmt.Println("That's too low")
} else if n > 5 {
	fmt.Println("That's too high")
} else {
	fmt.Println("That's just right", n)
}

We can also write this like:

if n := rand.Intn(10); n == 0 {
	fmt.Println("That's too low")
} else if n == 1 {
	fmt.Println("That's too high")
} else {
	fmt.Println("That's just right", n)
}

fmt.Println(n) // this causes a compilation error (undefined: n)

For loop

There are four ways to use a for loop in Go

The Complete for Statement

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

This is similar to how other languages write for loops, and this will print 0→9 inclusive.

The Condition-Only for Statement

i := 1
for i < 100 {
  fmt.Println(i)
  i = i * 2
}

This will print 1,2,4,8,16,…

The Infinite for Statement

for {
  fmt.Println("Hello")
}

This prints Hello an infinite amount of times. We can get out of these with a break or continue statement.

i := 1

for {
  fmt.Println(i)
  i = i * 2
  if i > 10 {
    break
  }
}

We can use continue to skip the current iteration and go to the next one. We can write a simple FizzBuzz calculator like so:

for i := 1; i <= 100; i++ {
		if i%3 == 0 && i%5 == 0 {
			fmt.Println("FizzBuzz")
		} else if i%3 == 0 {
			fmt.Println("Fizz")
		} else if i%5 == 0 {
			fmt.Println("Buzz")
		} else {
			fmt.Println(i)
		}
	}

Or we could write it with continue statements, which is arguably more readable:

for i := 1; i <= 100; i++ {
		if i%3 == 0 && i%5 == 0 {
			fmt.Println("FizzBuzz")
			continue
		}
		if i%3 == 0 {
			fmt.Println("Fizz")
			continue
		}
		if i%5 == 0 {
			fmt.Println("Buzz")
			continue
		}
		fmt.Println(i)
	}

The for-range Statement

We can iterate over elements in strings, arrays, slices and maps with a for-range loop

evenVals := []int{2, 4, 6, 8, 10, 12}
for i, v := range evenVals {
		fmt.Println(i, v)
}

// 0 2
// 1 4
// 2 6
// ...

If we don’t need the key in a for-range loop, we can use the _ to “ignore” it

evenVals := []int{2, 4, 6, 8, 10, 12}

for _, v := range evenVals {
		fmt.Println(v)
}

If you want the key and not the value, you can just leave off the second variable:

uniqueNames := map[string]bool{"Fred": true, "Raul": true, "John": true, "Jane": true}

for k := range uniqueNames {
		fmt.Println("Key: ", k)
}

Iterating over maps

We have to note that something odd happens when we iterate over maps:

n := map[string]int {
	"a": 1,
	"c": 3,
	"b": 2,
}

for i:= 0; i < 3; i++ {
	fmt.Println("Loop", i)
	for k, v := range n {
		fmt.Println(k, v)
	}
}

When you build and run the program, the output varies but here is one possibility:

Loop 0
c 3
b 2
a 1
Loop 1
a 1
c 3
b2
Loop 2
b 2
a 1
c 3

The order of the keys and values varies, and this is actually a security feature. In earlier Go versions, the iteration order for keys in a map was usually (not always) the same if you inserted the same items into a map. This caused two issues:

To prevent these issues, the Go team made two changes to the map implementation.

  1. They modified the hash algorithm to include a random number that’s generated every time a map variable is created.
  2. They made the order of a for-range iteration over a map vary a bit each time the map is loop over, making it far harder to implement a ****Hash DoS******** attack.

Iterating over Strings

samples := []string{"word", "duck 🪿!"}
	for _, sample := range samples {
		//	loop over the word:
		for i, r := range sample {
			fmt.Println(i, r, string(r))
		}
	}

The output where we iterate over “hello” has no surprises

0 119 w
1 111 o
2 114 r
3 100 d

But when we loop over duck with an emoji, we see that it skips from 5 to 9:

0 100 d
1 117 u
2 99 c
3 107 k
4 32
5 129727 🪿
9 33 !

This is because we are iterating over the runes, not the bytes.

The for-range Value is a Copy

You should note that every time the for-range loop iterates over your compound your compound type, the copies the value from the compound type to the value variable. Modifying the value variable will not modify the value in the compound type.

nums := []int{1, 2, 3, 4, 5}
for _, v := range nums {
		v *= 2
}
fmt.Println(nums) // [1 2 3 4 5]

Labelling for Statements

By default, the break and continue keywords apply to the for loop that directly contains them. When we have nested for loops and we want to exit the outer loop, we can use a label.

func main() {
	samples := []string{"hello", "word_l_word"}
outer:
	for _, sample := range samples {
		for i, r := range sample {
			fmt.Println(i, r, string(r))
			if r == 'l' {
				continue outer
			}
		}
	}
}

This returns:

0 104 h
1 101 e
2 108 l // continue here

0 119 w
1 111 o
2 114 r
3 100 d
4 95 _
5 108 l // continue here

Switch Statements

While other languages have very limited switch statements, Go has some slightly different behaviours.

words := []string{"Hello", "Word", "Octopus", "Hello Again"}
	for _, word := range words {
		switch size := len(word); size {
		case 1, 2, 3, 4:
			fmt.Println(word, " is a short word!")
		case 5:
			wordLen := len(word)
			fmt.Println(word, "is exactly the right length:", wordLen)
		case 6, 7, 8, 9:
		default:
			fmt.Println(word, "is a long word!")
		}
	}

// Hello is exactly the right length: 5
// Word  is a short word!
// Hello Again is a long word!

Confusing break Statements

In the example here, we put a break statement on the switch statement when the number reaches 7 - but it doesn’t break out of the loop here.

for i := 0; i < 10; i++ {
		switch {
		case i%2 == 0:
			fmt.Println(i, "is even")
		case i%3 == 0:
			fmt.Println(i, "is divisible by 3 but not 2")
		case i%7 == 0:
			fmt.Println("exit the loop!")
			break
		default:
			fmt.Println(i, "is boring")
		}
	}

// 0 is even
// 1 is boring
// 2 is even
// 3 is divisible by 3 but not 2
// 4 is even
// 5 is boring
// 6 is even
// exit the loop!
// 8 is even
// 9 is divisible by 3 but not 2

We have to use a label to break out of the loop here

meow:
	for i := 0; i < 10; i++ {
		switch {
		case i%2 == 0:
			fmt.Println(i, "is even")
		case i%3 == 0:
			fmt.Println(i, "is divisible by 3 but not 2")
		case i%7 == 0:
			fmt.Println("exit the loop!")
			break meow
		default:
			fmt.Println(i, "is boring")
		}
	}

// 0 is even
// 1 is boring
// 2 is even
// 3 is divisible by 3 but not 2
// 4 is even
// 5 is boring
// 6 is even
// exit the loop!

Here we use the label meow and break out of the meow loop and it works as intended.

Blank Switches

We can compare with a blank switch:

words := []string{"hi", "salutations", "hello"}
	for _, word := range words {
		switch wordLen := len(word); {
		case wordLen < 5:
			fmt.Println(word, "is a short word!")
		case wordLen > 10:
			fmt.Println(word, "is a long word!")
		default:
			fmt.Println(word, "is exactly the right length.")
		}
	}

// hi is a short word!
// salutations is a long word!
// hello is exactly the right length.

Just like a regular switch statement, you can include a short variable declaration as part of your blank switch. But unlike a regular switch, you can write logical tests for your cases:

switch {
		case a == 2:
				fmt.Println("a is 2")
		case a == 3:
				fmt.Println("a is 3")
		default:
				fmt.Println("a is ", a)
}

Can be written in a simpler way (not a blank switch):

switch a {
		case 2:
				fmt.Println("a is 2")
		case 3:
				fmt.Println("a is 3")
		default:
				fmt.Println("a is ", a)
}

Choosing Between if and switch

There isn’t a lot of difference between a series of if/else statements and a blank switch statement, they both allow a series of comparisons. So when should you use them?

A switch statement implies there is a relationship between the values or comparisons in each case.

switch n := rand.Intn(10); {
case n == 0:
		fmt.Println("That's too low")
case n > 5:
		fmt.Println("That's too big:", n)
default:
		fmt.Println("That's a good number:", n)
}

This is arguably more readable, but moth methods are acceptable.

Note: it is not idiomatic not do unrelated comparisons in a blank switch statement.

goto Statement

Traditionally goto statements were considered dangerous because it could jump to nearly anywhere in the program - in or out of a loop, skip over variable definitions or into the middle of a set of statements in an if statement. This made it difficult to understand what a goto-using program actually did.

func main() {
	a := 10
	goto skip
	b := 20
skip:
	c := 30
	fmt.Println(a, b, c)
	if c > a {
		goto inner
	}
	if a < b {
	inner:
		fmt.Println("a is less than b")
	}
}

Go will fail to build the above code with the following errors:

goto skip jumps over declaration of b at ./prog.go:8:4
goto inner jumps into block starting at ./prog.go:15:11

So what could we use goto for? Labeled break and continue statements allow you to jump out of deeply nested loops or skip iteration.

func main() {
		a := rand.Intn(10)
		for a < 100 {
				if a%5 == 0 {
						goto done
				}
				a = a*2 + 1
		}
		fmt.Println("do something when the loop completes normally")
done:
		fmt.Println("do complicated stuff no matter why we left the loop")
		fmt.println(a)
}

// do complicated stuff no matter why we left the loop
// 55

This shows that in a simple case, there is some logic that we don’t want to run in the middle of the function, but we do want to run the end of the function.

A real world example would be in the floatBits method in atof.go in the strconv package in the standard library. The method ends with this code:

overflow:
		// ±Inf
		mant = 0
		exp = 1<<flt.expbits - 1 + flt.bias
		overflow = true

out:
		// Assemble bits.
		bits := mant & (uint64(1)<<flt.mantbits - 1)
		bits |= uint64((exp-flt.bias)&(1<<flt.expbits-1)) << flt.mantbits
		if d.neg {
				bits |= 1 << flt.mantbits << flt.expbits
		}
		return bits, overflow
}

Before these lines, there are several condition checks. Some require the code after the overflow label to run, while other conditions require skipping that code and going directly to out.

Depending on the condition, there are goto statements that jump to overflow or out. You could probably come up with a way to avoid the goto statements, but they all make the code harder to understand.

Prev