Chapter 4. Blocks, Shadows, and Control Structures
Now that we have covered variables, constants, and built-in types, we are ready to look at programming logic and organization. Weâll start by explaining blocks and how they control when an identifier is available. Then weâll look at Goâs control structures: if
, for
, and switch
. Finally, we will talk about goto
and the one situation when you should use it.
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.
Note
So far, weâve only written the main
function, but weâll write functions with parameters in the next chapter.
Each place where a declaration occurs is called a block. Variables, constants, types, and functions declared outside of any functions are placed in the package block. Weâve used import
statements in our programs to gain access to printing and math functions (and will talk about them in detail in Chapter 9). They define names for other packages that are valid for the file that contains the import
statement. These names are in the file block. All of the variables defined at the top level of a function (including the parameters to a function) are in a block. Within a function, every set of braces ({}
) defines another block, and in a bit we will see that the control structures in Go define blocks of their own.
You can access an identifier defined in any outer block from within any inner block. This raises the question: what happens when you have a declaration with the same name as an identifier in a containing block? If you do that, you shadow the identifier created in the outer block.
Shadowing Variables
Before explaining what shadowing is, letâs take a look at some code (see Example 4-1). You can run it on The Go Playground.
Example 4-1. Shadowing variables
func
main
()
{
x
:=
10
if
x
>
5
{
fmt
.
Println
(
x
)
x
:=
5
fmt
.
Println
(
x
)
}
fmt
.
Println
(
x
)
}
Before you run this code, try to guess what itâs going to print out:
-
Nothing prints; the code does not compile
-
10 on line one, 5 on line two, 5 on line three
-
10 on line one, 5 on line two, 10 on line three
Hereâs what happens:
10 5 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 cannot access a shadowed variable.
In this case, we almost certainly didnât want to create a brand-new x
inside the if
statement. Instead, we probably wanted to assign 5 to the x
declared at the top level of the function block. At the first fmt.Println
inside the if
statement, we are able to access the x
declared at the top level of the function. On the next line, though, we shadow x
by declaring a new variable with the same name inside the block created by the if
statementâs body. At the second fmt.Println
, when we access the variable named x
, we get the shadowing variable, which has the value of 5. The closing brace for the if
statementâs body ends the block where the shadowing x
exists, and at the third fmt.Println
, when we access the variable named x
, we get the variable declared at the top level of the function, which has the value of 10. Notice that this x
didnât disappear or get reassigned; there was just no way to access it once it was shadowed in the inner block.
I mentioned in the last chapter that there are situations where I avoid using :=
because it can make it unclear what variables are being used. Thatâs because it is very easy to accidentally shadow a variable when using :=
. Remember, we can use :=
to create and assign to multiple variables at once. Also, not all of the variables on the lefthand side have to be new for :=
to be legal. You can use :=
as long as there is at least one new variable on the lefthand side. Letâs look at another program (see Example 4-2), which can be found on The Go Playground.
Example 4-2. Shadowing with multiple assignment
func
main
()
{
x
:=
10
if
x
>
5
{
x
,
y
:=
5
,
20
fmt
.
Println
(
x
,
y
)
}
fmt
.
Println
(
x
)
}
Running this code gives you:
5 20 10
Although there was an existing definition of x
in an outer block, x
was still shadowed within the if
statement. Thatâs because :=
only reuses variables that are declared in the current block. When using :=
, make sure that you donât have any variables from an outer scope on the lefthand side, unless you intend to shadow them.
You also need to be careful to ensure that you donât shadow a package import. Weâll talk more about importing packages in Chapter 9, but weâve been importing the fmt
package to print out results of our programs. Letâs see what happens when we declare a variable called fmt
within our main
function, as shown in Example 4-3. You can try to run it on The Go Playground.
Example 4-3. Shadowing package names
func
main
()
{
x
:=
10
fmt
.
Println
(
x
)
fmt
:=
"oops"
fmt
.
Println
(
fmt
)
}
When we try to run this code, we get an error:
fmt.Println undefined (type string has no field or method Println)
Notice that the problem isnât that we named our variable fmt
, itâs that we tried to access something that the local variable fmt
didnât have. Once the local variable fmt
is declared, it shadows the package named fmt
in the file block, making it impossible to use the fmt
package for the rest of the main
function.
Detecting Shadowed Variables
Given the subtle bugs that shadowing can introduce, itâs a good idea to make sure that you donât have any shadowed variables in your programs. Neither go vet
nor golangci-lint
include a tool to 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, consider including shadow
in the vet
task:
vet
:
go vet ./... shadow ./....PHONY
:
vet
When you run make vet
against the previous code, youâll see that the shadowed variable is now detected:
declaration of "x" shadows declaration at line 6
if  Â
The if
statement in Go is much like the if
statement in most programming languages. Because it is such a familiar construct, Iâve used it in early sample code without worrying that itâd be confusing. Example 4-5 shows a more complete sample.
Example 4-5. if
and else
n
:=
rand
.
Intn
(
10
)
if
n
==
0
{
fmt
.
Println
(
"That's too low"
)
}
else
if
n
>
5
{
fmt
.
Println
(
"That's too big:"
,
n
)
}
else
{
fmt
.
Println
(
"That's a good number:"
,
n
)
}
Note
If you run this code, youâll find that it always assigns 1 to n
. This happens because the default random number seed in math/rand
is hard-coded. In âOverriding a Packageâs Nameâ, weâll look at a way to ensure a good seed for random number generation while demonstrating how to handle package name collisions.
The most visible difference between if
statements in Go and other languages is that you donât put parenthesis around the condition. But thereâs another feature that Go adds to if
statements that helps you better manage your variables.
As we discussed in the section on shadowing variables, any variable declared within the braces of an if
or else
statement exists only within that block. This isnât that uncommon; it is true in most languages. What Go adds is the ability to declare variables that are scoped to the condition and to both the if
and else
blocks. Letâs take a look by rewriting our previous example to use this scope, as shown in Example 4-6.
Example 4-6. Scoping a variable to an if
statement
if
n
:=
rand
.
Intn
(
10
);
n
==
0
{
fmt
.
Println
(
"That's too low"
)
}
else
if
n
>
5
{
fmt
.
Println
(
"That's too big:"
,
n
)
}
else
{
fmt
.
Println
(
"That's a good number:"
,
n
)
}
Having this special scope is very handy. It lets you create variables that are available only where they are needed. Once the series of if/else
statements ends, n
is undefined. You can test this by trying to run the code in Example 4-7 on
The Go Playground.
Example 4-7. Out of scopeâ¦
if
n
:=
rand
.
Intn
(
10
);
n
==
0
{
fmt
.
Println
(
"That's too low"
)
}
else
if
n
>
5
{
fmt
.
Println
(
"That's too big:"
,
n
)
}
else
{
fmt
.
Println
(
"That's a good number:"
,
n
)
}
fmt
.
Println
(
n
)
Attempting to run this code produces a compilation error:
undefined: n
Note
Technically, you can put any simple statement before the comparison in an if
statement. This includes things like a function call that doesnât return a value or assigning a new value to an existing variable. But donât do this. Only use this feature to define new variables that are scoped to the if/else
statements; anything else would be confusing.
Also be aware that just like any other block, a variable declared as part of an if
statement will shadow variables with the same name that are declared in containing blocks.
for, Four Ways
As in other languages in the C family, Go uses a for
statement to loop. What makes Go different from other languages is that for
is the only looping keyword in the language. Go accomplishes this by using the for
keyword in four different formats:
-
A complete, C-style
for
-
A condition-only
for
-
An infinite
for
-
for-range
The Complete for Statement
The first one weâll look at is the complete for
declaration you might be familiar with from C, Java, or JavaScript, as shown in Example 4-8.
Example 4-8. A complete for
statement
for
i
:=
0
;
i
<
10
;
i
++
{
fmt
.
Println
(
i
)
}
You would probably be unsurprised to find that this program prints out the numbers from 0 to 9, inclusive.
Just like the if
statement, there are no parenthesis around the parts of the for
statement. Otherwise, it should look very familiar. There are three parts, separated by semicolons. The first is an initialization that sets one or more variables before the loop begins. There are two important details to remember about the initialization section. First, you must use :=
to initialize the variables; var
is not legal here. Second, just like variable declarations in if
statements, you can shadow a variable here.
The second part is the comparison. This must be an expression that evaluates to a bool
. It is checked immediately before the loop body runs, after the initialization, and after the loop reaches the end. If the expression evaluates to true
, the loop body is executed.
The last part of a standard for
statement is the increment. You usually see something like i++ here, but any assignment is valid. It runs immediately after each iteration of the loop, before the condition is evaluated.
The Condition-Only for Statement
Go allows you to leave off both the initialization and the increment in a for
statement. That leaves a for
statement that functions like the while
statement found in C, Java, JavaScript, Python, Ruby, and many other languages. It looks like Example 4-9.
Example 4-9. A condition-only for
statement
i
:=
1
for
i
<
100
{
fmt
.
Println
(
i
)
i
=
i
*
2
}
The Infinite for Statement
The third for
statement format does away with the condition, too. Go has a version of a for
loop that loops forever. If you learned to program in the 1980s, your first program was probably an infinite loop in BASIC that printed HELLO to the screen forever:
10
"HELLO"
20
GOTO
10
Example 4-10 shows the Go version of this program. You can run it locally or try it out on The Go Playground.
Example 4-10. Infinite looping nostalgia
package
main
import
"fmt"
func
main
()
{
for
{
fmt
.
Println
(
"Hello"
)
}
}
Running this program gives you the same output that filled the screens of millions of Commodore 64s and Apple ][s:
Hello Hello Hello Hello Hello Hello Hello ...
Press Ctrl-C when you are tired of walking down memory lane.
Note
If you run this on The Go Playground, youâll find that it will stop execution after a few seconds. As a shared resource, the playground doesnât allow any one program to run for too long.
break and continue
How do you get out of an infinite for
loop without using the keyboard or turning off your computer? Thatâs the job of the break
statement. It exits the loop immediately, just like the break
statement in other languages. Of course, you can use break
with any for
statement, not just the infinite for
statement.
Note
There is no Go equivalent of the do
keyword in Java, C, and JavaScript. If you want to iterate at least once, the cleanest way is to use an infinite for
loop that ends with an if
statement. If you have some Java code, for example, that uses a do/while
loop:
do
{
// things to do in the loop
}
while
(
CONDITION
);
The Go version looks like this:
for
{
// things to do in the loop
if
!
CONDITION
{
break
}
}
Note that the condition has a leading !
to negate the condition from the Java code. The Go code is specifying how to exit the loop, while the Java code specifies how to stay in it.
Go also includes the continue
keyword, which skips over the rest of the body of a for
loop and proceeds directly to the next iteration. Technically, you donât need a
continue
statement. You could write code like Example 4-11.
Example 4-11. Confusing code
for
i
:=
1
;
i
<=
100
;
i
++
{
if
i
%
3
==
0
{
if
i
%
5
==
0
{
fmt
.
Println
(
"FizzBuzz"
)
}
else
{
fmt
.
Println
(
"Fizz"
)
}
}
else
if
i
%
5
==
0
{
fmt
.
Println
(
"Buzz"
)
}
else
{
fmt
.
Println
(
i
)
}
}
But this is not idiomatic. Go encourages short if
statement bodies, as left-aligned as possible. Nested code is difficult to follow. Using a continue
statement makes it easier to understand whatâs going on. Example 4-12 shows the code from the previous example, rewritten to use continue
instead.
Example 4-12. Using continue
to make code clearer
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
)
}
As you can see, replacing chains of if/else
statements with a series of if
statements that use continue
makes the conditions line up. This improves the layout of your conditions, which means your code is easier to read and understand.
The for-range Statement
The fourth for
statement format is for iterating over elements in some of Goâs built-in types. It is called a for-range
loop and resembles the iterators found in other languages. In this section, we will look at how to use a for-range
loop with strings, arrays, slices, and maps. When we cover channels in Chapter 10, we will talk about how to use them with for-range
loops.
Note
You can only use a for-range
loop to iterate over the built-in compound types and user-defined types that are based on them.
First, letâs take a look at using a for-range
loop with a slice. You can try out the code in Example 4-13 on The Go Playground.
Example 4-13. The for-range
loop
evenVals
:=
[]
int
{
2
,
4
,
6
,
8
,
10
,
12
}
for
i
,
v
:=
range
evenVals
{
fmt
.
Println
(
i
,
v
)
}
Running this code produces the following output:
0 2 1 4 2 6 3 8 4 10 5 12
What makes a for-range
loop interesting is that you get two loop variables. The first variable is the position in the data structure being iterated, while the second is the value at that position. The idiomatic names for the two loop variables depend on what is being looped over. When looping over an array, slice, or string, an i
for index is commonly used. When iterating through a map, k
(for key) is used instead.
The second variable is frequently called v
for value, but is sometimes given a name based on the type of the values being iterated. Of course, you can give the variables any names that you like. If there are only a few statements in the body of the loop, single letter variable names work well. For longer (or nested) loops, youâll want to use more descriptive names.
What if you donât need to use the key within your for-range
loop? Remember, Go requires you to access all declared variables, and this rule applies to the ones declared as part of a for
loop, too. If you donât need to access the key, use an underscore (_
) as the variableâs name. This tells Go to ignore the value. Letâs rewrite our slice ranging code to not print out the position. We can run the code in Example 4-14 on The Go Playground.
Example 4-14. Ignoring the key in a for-range
loop
evenVals
:=
[]
int
{
2
,
4
,
6
,
8
,
10
,
12
}
for
_
,
v
:=
range
evenVals
{
fmt
.
Println
(
v
)
}
Running this code produces the following output:
2 4 6 8 10 12
Tip
Any time you are in a situation where thereâs a value returned, but you want to ignore it, use an underscore to hide the value. Youâll see the underscore pattern again when we talk about functions in Chapter 5 and packages in Chapter 9.
What if you want the key, but donât want the value? In this situation, Go allows you to just leave off the second variable. This is valid Go code:
uniqueNames
:=
map
[
string
]
bool
{
"Fred"
:
true
,
"Raul"
:
true
,
"Wilma"
:
true
}
for
k
:=
range
uniqueNames
{
fmt
.
Println
(
k
)
}
The most common reason for iterating over the key is when a map is being used as a set. In those situations, the value is unimportant. However, you can also leave off the value when iterating over arrays or slices. This is rare, as the usual reason for iterating over a linear data structure is to access the data. If you find yourself using this format for an array or slice, thereâs an excellent chance that you have chosen the wrong data structure and should consider refactoring.
Note
When we look at channels in Chapter 10, weâll see a situation where a for-range
loop only returns a single value each time the loop iterates.
Iterating over maps
Thereâs something interesting about how a for-range
loop iterates over a map. You can run the code in Example 4-15 on The Go Playground.
Example 4-15. Map iteration order varies
m
:=
map
[
string
]
int
{
"a"
:
1
,
"c"
:
3
,
"b"
:
2
,
}
for
i
:=
0
;
i
<
3
;
i
++
{
fmt
.
Println
(
"Loop"
,
i
)
for
k
,
v
:=
range
m
{
fmt
.
Println
(
k
,
v
)
}
}
When you build and run this program, the output varies. Here is one possibility:
Loop 0 c 3 b 2 a 1 Loop 1 a 1 c 3 b 2 Loop 2 b 2 a 1 c 3
The order of the keys and values varies; some runs may be identical. This is actually a security feature. In earlier Go versions, the iteration order for keys in a map was usually (but not always) the same if you inserted the same items into a map. This caused two problems:
-
People would write code that assumed that the order was fixed, and this would break at weird times.
-
If maps always hash items to the exact same values, and you know that a server is storing some user data in a map, you can actually slow down a server with an attack called Hash DoS by sending it specially crafted data where all of the keys hash to the same bucket.
To prevent both of these problems, the Go team made two changes to the map implementation. First, they modified the hash algorithm for maps to include a random number thatâs generated every time a map variable is created. Next, they made the order of a for-range
iteration over a map vary a bit each time the map is looped over. These two changes make it far harder to implement a Hash DoS attack.
Note
There is one exception to this rule. To make it easier to debug and log maps, the formatting functions (like fmt.Println
) always output maps with their keys in ascending sorted order.
Iterating over strings
As I mentioned earlier, you can also use a string with a for-range
loop. Letâs take a look. You can run the code in Example 4-16 on your computer or on The Go Playground.
Example 4-16. Iterating over strings
samples
:=
[]
string
{
"hello"
,
"apple_Ï!"
}
for
_
,
sample
:=
range
samples
{
for
i
,
r
:=
range
sample
{
fmt
.
Println
(
i
,
r
,
string
(
r
))
}
fmt
.
Println
()
}
The output when we iterate over the word âhelloâ has no surprises:
0 104 h 1 101 e 2 108 l 3 108 l 4 111 o
In the first column, we have the index; in the second, the numeric value of the letter; and in the third, we have the numeric value of the letter type converted to a string.
Looking at the result for âapple_Ï!â is more interesting:
0 97 a 1 112 p 2 112 p 3 108 l 4 101 e 5 95 _ 6 960 Ï 8 33 !
There are two things to notice. First, notice that the first column skips the number 7. Second, the value at position 6 is 960. Thatâs far larger than what can fit in a byte. But in Chapter 3, we said that strings were made out of bytes. Whatâs going on?
What we are seeing is special behavior from iterating over a string with a for-range
loop. It iterates over the runes, not the bytes. Whenever a for-range
loop encounters a multibyte rune in a string, it converts the UTF-8 representation into a single 32-bit number and assigns it to the value. The offset is incremented by the number of bytes in the rune. If the for-range
loop encounters a byte that doesnât represent a valid UTF-8 value, the Unicode replacement character (hex value 0xfffd) is returned instead.
Tip
Use a for-range
loop to access the runes in a string in order. The key is the number of bytes from the beginning of the string, but the type of the value is rune.
The for-range value is a copy
You should be aware that each time the for-range
loop iterates over your compound type, it copies the value from the compound type to the value variable. Modifying the value variable will not modify the value in the compound type. Example 4-17 shows a quick program to demonstrate this. You can try it out on The Go Playground.
Example 4-17. Modifying the value doesnât modify the source
evenVals
:=
[]
int
{
2
,
4
,
6
,
8
,
10
,
12
}
for
_
,
v
:=
range
evenVals
{
v
*=
2
}
fmt
.
Println
(
evenVals
)
Running this code gives the following output:
[2 4 6 8 10 12]
The implications of this behavior are subtle. When we talk about goroutines in Chapter 10, youâll see that if you launch goroutines in a for-range
loop, you need to be very careful in how you pass the index and value to the goroutines, or youâll get surprisingly wrong results.
Just like the other three forms of the for
statement, you can use break
and continue
with a for-range
loop.
Labeling Your for Statements
By default, the break
and continue
keywords apply to the for
loop that directly contains them. What if you have nested for
loops and you want to exit or skip over an iterator of an outer loop? Letâs look at an example. Weâre going to modify our earlier string iterating program to stop iterating through a string as soon as it hits a letter âl.â You can run the code in Example 4-18 on The Go Playground.
Example 4-18. Labels
func
main
()
{
samples
:=
[]
string
{
"hello"
,
"apple_Ï!"
}
outer
:
for
_
,
sample
:=
range
samples
{
for
i
,
r
:=
range
sample
{
fmt
.
Println
(
i
,
r
,
string
(
r
))
if
r
==
'l'
{
continue
outer
}
}
fmt
.
Println
()
}
}
Notice that the label outer
is indented by go fmt
to the same level as the surrounding function. Labels are always indented to the same level as the braces for the block. This makes them easier to notice. Running our program gives the following output:
0 104 h 1 101 e 2 108 l 0 97 a 1 112 p 2 112 p 3 108 l
Nested for
loops with labels are rare. They are most commonly used to implement algorithms similar to the pseudocode below:
outer
:
for
_
,
outerVal
:=
range
outerValues
{
for
_
,
innerVal
:=
range
outerVal
{
// process innerVal
if
invalidSituation
(
innerVal
)
{
continue
outer
}
}
// here we have code that runs only when all of the
// innerVal values were sucessfully processed
}
Choosing the Right for Statement
Now that weâve covered all of the forms of the for
statement, you might be wondering when to use which format. Most of the time, youâre going to use the for-range
format. A for-range
loop is the best way to walk through a string, since it properly gives you back runes instead of bytes. We have also seen that a for-range
loop works well for iterating through slices and maps, and weâll see in Chapter 10 that channels work naturally with for-range
as well.
Tip
Favor a for-range
loop when iterating over all the contents of an instance of one of the built-in compound types. It avoids a great deal of boilerplate code thatâs required when you use an array, slice, or map with one of the other for
loop styles.
When should you use the complete for
loop? The best place for it is when you arenât iterating from the first element to the last element in a compound type. While you could use some combination of if
, continue
, and break
within a for-range
loop, a standard for
loop is a clearer way to indicate the start and end of your iteration. Compare these two code snippets, both of which iterate over the second through the second-to-last elements in an array. First the for-range
loop:
evenVals
:=
[]
int
{
2
,
4
,
6
,
8
,
10
}
for
i
,
v
:=
range
evenVals
{
if
i
==
0
{
continue
}
if
i
==
len
(
evenVals
)
-
1
{
break
}
fmt
.
Println
(
i
,
v
)
}
And hereâs the same code, with a standard for
loop:
evenVals
:=
[]
int
{
2
,
4
,
6
,
8
,
10
}
for
i
:=
1
;
i
<
len
(
evenVals
)
-
1
;
i
++
{
fmt
.
Println
(
i
,
evenVals
[
i
])
}
The standard for
loop code is both shorter and easier to understand.
Warning
This pattern does not work for skipping over the beginning of a string. Remember, a standard for
loop doesnât properly handle multibyte characters. If you want to skip over some of the runes in a string, you need to use a for-range
loop so that it will properly process runes for you.
The remaining two for
statement formats are used less frequently. The condition-only for
loop is, like the while
loop it replaces, useful when you are looping based on a calculated value.
The infinite for
loop is useful in some situations. There should always be a break
somewhere within the body of the for
loop since itâs rare that you want to loop forever. Real-world programs should bound iteration and fail gracefully when operations cannot be completed. As shown previously, an infinite for
loop can be combined with an if
statement to simulate the do
statement thatâs present in other languages. An infinite for
loop is also used to implement some versions of the iterator pattern, which we will look at when we review the standard library in âio and Friendsâ.
switch
Like many C-derived languages, Go has a switch
statement. Most developers in those languages avoid switch
statements because of their limitations on values that can be switched on and the default fall-through behavior. But Go is different. It makes switch
statements useful.
Note
For those readers who are more familiar with Go, weâre going to cover expression switch statements in this chapter. Weâll discuss type switch statements when we talk about interfaces in Chapter 7.
At first glance, switch
statements in Go donât look all that different from how they appear in C/C++, Java, or JavaScript, but there are a few surprises. Letâs take a look
at a sample switch
statement. You can run the code in Example 4-19 on
The Go Playground.
Example 4-19. The switch
statement
words
:=
[]
string
{
"a"
,
"cow"
,
"smile"
,
"gopher"
,
"octopus"
,
"anthropologist"
}
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!"
)
}
}
When we run this code, we get the following output:
a is a short word! cow is a short word! smile is exactly the right length: 5 anthropologist is a long word!
Letâs go over the features of the switch
statement to explain the output. As is the case with if
statements, you donât put parenthesis around the value being compared in a switch
. Also like an if
statement, you can declare a variable thatâs scoped to all of the branches of the switch
statement. In our case, we are scoping the variable size
to all of the cases in the switch
statement.
All of the case
clauses (and the optional default
clause) are contained inside a set of braces. But you should note that you donât put braces around the contents of the case
clauses. You can have multiple lines inside a case
(or default
) clause and they are all considered to be part of the same block.
Inside case 5:
, we declare wordLen
, a new variable. Since this is a new block, you can declare new variables within it. Just like any other block, any variables declared within a case
clauseâs block are only visible within that block.
If you are used to putting a break
statement at the end of every case
in your switch
statements, youâll be happy to notice that they are gone. By default, cases in switch
statements in Go donât fall through. This is more in line with the behavior in Ruby or (if you are an old-school programmer) Pascal.
This prompts the question: if cases donât fall through, what do you do if there are multiple values that should trigger the exact same logic? In Go, you separate multiple matches with commas, like we do when matching 1, 2, 3, and 4 or 6, 7, 8, and 9. Thatâs why we get the same output for both a
and cow
.
Which leads to the next question: if you donât have fall-through, and you have an empty case (like we do in our sample program when the length of our argument is 6, 7, 8, or 9 characters), what happens? In Go, an empty case means nothing happens. Thatâs why we donât see any output from our program when we use octopus or gopher as the parameter.
Tip
For the sake of completeness, Go does include a fallthrough
keyword, which lets one case continue on to the next one. Please think twice before implementing an algorithm that uses it. If you find yourself needing to use fallthrough
, try to restructure your logic to remove the dependencies between cases.
In our sample program we are switching on the value of an integer, but thatâs not all you can do. You can switch on any type that can be compared with ==
, which includes all of the built-in types except slices, maps, channels, functions, and structs that contain fields of these types.
Even though you donât need to put a break
statement at the end of each case
clause, you can use them in situations where you want to exit early from a case
. However, the need for a break
statement might indicate that you are doing something too complicated. Consider refactoring your code to remove it.
There is one more place where you might find yourself using a break
statement in a case
in a switch
statement. If you have a switch
statement inside a for
loop, and you want to break out of the for
loop, put a label on the for
statement and put the name of the label on the break
. If you donât use a label, Go assumes that you want to break out of the case
. Letâs look at a quick example. You can run the code in Example 4-20 on The Go Playground.
Example 4-20. The case of the missing label
func
main
()
{
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"
)
}
}
}
Running this code produces the following output:
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
Thatâs not what we intended. The goal was to break out of the for
loop when we got a 7. To do this, we need to introduce a label, just like we did when breaking out of a nested for
loop. First, we label the for
statement:
loop
:
for
i
:=
0
;
i
<
10
;
i
++
{
Then we use the label on our break:
break
loop
You can see these changes on The Go Playground. When we run it again, we get the output that we expected:
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!
Blank Switches
Thereâs another, more powerful way to use switch
statements. Just like Go allows you to leave out parts from a for
statementâs declaration, you can write a switch
statement that doesnât specify the value that youâre comparing against. This is called a blank switch. A regular switch
only allows you to check a value for equality. A blank switch
allows you to use any boolean comparison for each case
. You can try out the code in Example 4-21 on The Go Playground.
Example 4-21. The 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."
)
}
}
When you run this program, you get the following output:
hi is a short word! salutations is a long word! hello is exactly the right length.
Just like a regular switch
statement, you can optionally include a short variable declaration as part of your blank switch
. But unlike a regular switch
, you can write logical tests for your cases. Blank switches are pretty cool, but donât overdo them. If you find that you have written a blank switch
where all of your cases are equality comparisons against the same variable:
switch
{
case
a
==
2
:
fmt
.
Println
(
"a is 2"
)
case
a
==
3
:
fmt
.
Println
(
"a is 3"
)
case
a
==
4
:
fmt
.
Println
(
"a is 4"
)
default
:
fmt
.
Println
(
"a is "
,
a
)
}
you should replace it with an expression switch
statement:
switch
a
{
case
2
:
fmt
.
Println
(
"a is 2"
)
case
3
:
fmt
.
Println
(
"a is 3"
)
case
4
:
fmt
.
Println
(
"a is 4"
)
default
:
fmt
.
Println
(
"a is "
,
a
)
}
Choosing Between if and switch
As a matter of functionality, there isnât a lot of difference between a series of if/else
statements and a blank switch
statement. Both of them allow a series of comparisons. So, when should you use switch
and when should you use a set of if/else
statements? A switch
statement, even a blank switch
, indicates that there is some relationship between the values or comparisons in each case. To demonstrate the difference in clarity, letâs rewrite our random number classifier from the section on if
using a switch
instead, as shown in Example 4-22.
Example 4-22. Rewriting if/else
with a blank switch
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
)
}
Most people would agree that this is more readable. The value being compared is listed on a line by itself, and all of the cases line up on the lefthand side. The regularity of the location of the comparisons makes them easy to follow and modify.
Of course, there is nothing in Go that prevents you from doing all sorts of unrelated comparisons on each case in a blank switch
. However, this is not idiomatic. If you find yourself in a situation where you want to do this, use a series of if/else
statements (or perhaps consider refactoring your code).
gotoâYes, goto
There is a fourth control statement in Go, but chances are, you will never use it. Ever since Edgar Dijkstra wrote âGo To Statement Considered Harmfulâ in 1968, the goto
statement has been the black sheep of the coding family. There are good reasons for this. Traditionally, goto
was dangerous because it could jump to nearly anywhere in a program; you could jump into 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 did.
Most modern languages donât include goto
. Yet Go has a goto
statement. You should still do what you can to avoid using it, but it has some uses, and the limitations that Go places on it make it a better fit with structured programming.
In Go, a goto
statement specifies a labeled line of code and execution jumps to it. However, you canât jump anywhere. Go forbids jumps that skip over variable declarations and jumps that go into an inner or parallel block.
The program in Example 4-23 shows two illegal goto
statements. You can attempt to run it on The Go Playground.
Example 4-23. Goâs goto
has rules
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"
)
}
}
Trying to run this program produces the following errors:
goto skip jumps over declaration of b at ./main.go:8:4 goto inner jumps into block starting at ./main.go:15:11
So what should you use goto
for? Mostly, you shouldnât. Labeled break
and continue
statements allow you to jump out of deeply nested loops or skip iteration. The program in Example 4-24 has a legal goto
and demonstrates one of the few valid use cases.
Example 4-24. A reason to use goto
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
)
}
This example is contrived, but it shows how goto
can make a program clearer. In our 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. There are ways to do this without goto
. We could set up a boolean flag or duplicate the complicated code after the for
loop instead of having a goto
, but there are drawbacks to both of these approaches. Littering your code with boolean flags to control the logic flow is arguably the same functionality as the goto
statement, just more verbose. Duplicating complicated code is problematic because it makes your code harder to maintain. These situations are rare, but if you cannot find a way to restructure your logic, using a goto
like this actually improves your code.
If you want to see a real-world example, you can take a look at the floatBits
method in the file atof.go in the strconv
package in the standard library. Itâs too long to include in its entirety, but 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.
Wrapping Up
This chapter covered a lot of important topics for writing idiomatic Go. Weâve learned about blocks, shadowing, and control structures, and how to use them correctly. At this point, weâre able to write simple Go programs that fit within the main function. Itâs time to move on to larger programs, using functions to organize our code.
Get Learning Go now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.