18 if Statements, for Loops, and switch Statements #
In the previous two articles, I mainly explained the knowledge and techniques related to go
statements, goroutines, and the Go language scheduler.
There is a lot of content, and you don’t have to completely digest it all at once. You can gradually understand and consolidate it in the process of programming practice.
Now, let’s temporarily step down from the altar and return to the ordinary world. Today, I will talk about If statements, For statements, and Switch statements, which are all basic flow control statements in the Go language. Their syntax looks simple, but there are also some tricks and considerations. In this article, I will use a series of interview questions as clues to explain their usage.
So, today’s question is: What details do you need to pay attention to when using a For statement with a range
clause? This is a relatively general question. I will explain it through coding exercises.
The code in this question is placed in the
main
function of the command source code filedemo41.go
. To focus on the question itself, some code package declaration statements, code package import statements, and the declaration part of the main function are omitted in this article.
I first declare a variable numbers1
of slice type with the element type of int
. This slice has 6 elements, which are integers from 1
to 6
. I use a For statement with a range
clause to iterate over all the elements in the numbers1
variable.
In this For statement, there is only one iteration variable i
. In each iteration, I first check if the value of i
is equal to 3
. If the result is true
, then I use the bitwise OR operation between the i
itself and the i
th element value of numbers1
, and assign the result as the new i
th element value of numbers1
. Finally, I will print the value of numbers1
.
So the specific question is, what will be printed after executing this code?
The typical answer here is: The printed content will be [1 2 3 7 5 6]
.
Problem Analysis #
Is the answer you calculated in your mind like this? Let’s reproduce the calculation process together.
When the for
statement is executed, the expression on the right side of the range
keyword, numbers1
, is evaluated first.
The code at this position is called the range
expression. The result of the range
expression can be an array, a pointer to an array, a slice, a string, a dictionary, or one that allows receiving operations in a channel, and there can only be one result value.
For different types of result values of the range
expression, the number of iteration variables in the for
statement can be different.
Take numbers1
in our case as an example, it is a slice, so there can be two iteration variables. The iteration variable on the right represents the value of the current iteration, and the iteration variable on the left represents the index of the element value in the slice.
So, what does it mean when there is only one iteration variable, like in the code in this problem? This means that the iteration variable only represents the index value of the element value for the current iteration.
In a broader sense, when there is only one iteration variable, the element values of arrays, pointers to arrays, slices, and strings have nowhere to be placed, and we can only get index values given in ascending order.
Therefore, the value of the iteration variable i
here will be integers from 0
to 5
in sequence. When the value of i
is equal to 3
, the corresponding element value in the slice is the 4th element value 4
. The result of performing a bitwise OR operation on 4
and 3
is 7
. This is why the fourth integer in the answer is 7
.
Now, let me make a slight modification to the code above. Let’s estimate the printing content again.
numbers2 := [...]int{1, 2, 3, 4, 5, 6}
maxIndex2 := len(numbers2) - 1
for i, e := range numbers2 {
if i == maxIndex2 {
numbers2[0] += e
} else {
numbers2[i+1] += e
}
}
fmt.Println(numbers2)
Note that I have changed the object being iterated to numbers2
. The element values in numbers2
are also 6 integers from 1
to 6
, and the element type is also int
, but it is an array instead of a slice.
In the for
statement, I always assign a new value to the element immediately following the current iteration, which is the sum of these two elements. When iterating to the last element, I replace the original value of the first element in the result values of this range
expression with the sum of its original value and the last element value. Finally, I print the value of numbers2
.
For this piece of code, my question remains: what would be printed? You can think about it first.
Okay, it’s time to reveal the answer. The content that would be printed is [7 3 5 7 9 11]
. Let me reproduce the calculation process. When the for
statement is executed, the expression on the right side of the range
keyword, numbers2
, is evaluated first.
There are two things to note here:
- The
range
expression is only evaluated once when thefor
statement begins execution, regardless of how many iterations there will be afterwards. - The result of evaluating the
range
expression is copied. In other words, the object being iterated is a copy of the result value of therange
expression, not the original value.
Based on these two rules, let’s continue. In the first iteration, I change the value of the second element of numbers2
, making it 3
, which is the sum of 1
and 2
.
However, the second element of the iterated object remains unchanged, as it has become a completely unrelated array to numbers2
. Therefore, in the second iteration, I change the value of the third element of numbers2
to 5
, which is the sum of the second element value 2
and the third element value 3
.
Similarly, the element values of numbers2
will be 7
, 9
, and 11
in sequence. When iterating to the last element, I change the value of the first element of numbers2
to the sum of the original value of this range
expression’s first element and the value of the last element, which is 1
and 6
, respectively.
Okay, now it’s your turn. You need to change the value of numbers2
from an array to a slice, without changing any of the elements. To avoid confusion, you should assign this slice value to a new variable called numbers3
and replace all occurrences of numbers2
with numbers3
in the code below.
The question is, what will be printed after executing this modified code? If you can’t estimate it, you can try running it first and then try to explain the answer you see. Hint: Slices and arrays are different. The former is a reference type, while the latter is a value type.
We can continue discussing the rest of the content first, but I strongly recommend that you come back and think about and calculate the answer to the question I left you.
Extra Knowledge
Question 1: What is the relationship between the switch
expression and the case
expressions in a switch
statement?
Let’s take a look at some code:
value1 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch 1 + 3 {
case value1[0], value1[1]:
fmt.Println("0 or 1")
case value1[2], value1[3]:
fmt.Println("2 or 3")
case value1[4], value1[5], value1[6]:
fmt.Println("4 or 5 or 6")
}
First, I declared a variable value1
of array type, with elements of type int8
. In the switch
statement that follows, the code between the switch
keyword and the left curly brace {
is called the switch
expression. This switch
statement contains three case
clauses, and each case
clause contains a case
expression and a print statement.
A case
expression consists of the case
keyword and a list of expressions, with comma ,
separating multiple expressions. In the code above, case value1[0], value1[1]
is a case
expression with two sub-expressions represented by index expressions.
The other two case
expressions are case value1[2], value1[3]
and case value1[4], value1[5], value1[6]
.
In each case
clause, the print statements will print different contents, which represent the reason why the case
clause is selected. For example, printing 0 or 1
indicates that the current case
clause is selected because the result value of the switch
expression is equal to either 0
or 1
. The other two print statements will print 2 or 3
and 4 or 5 or 6
, respectively.
Now the question is, can this switch
statement with these three case
expressions compile successfully? If not, why? If yes, what will be printed after executing this switch
statement?
As I mentioned earlier, if the result value of the switch
expression is equal to any of the sub-expressions in a case
expression, the case
clause associated with that case
expression will be selected.
Once a case
clause is selected, the statements attached to it, after the case
expression, will be executed. At the same time, all other case
clauses will be ignored.
However, if the selected case
clause contains a fallthrough
statement in its statement list, the statements attached to the immediately following case
clause will also be executed.
Due to the existence of the aforementioned equality check operations (referred to as equality operations hereinafter), switch
statements have requirements for the result type of the switch
expression and the result types of the sub-expressions in each case
expression. After all, in Go, only values of the same type can be allowed to perform equality operations.
If the result value of the switch
expression is an untyped constant, such as the result of 1 + 3
, which is an untyped constant 4
, this constant will be automatically converted to a value of the default type for this kind of constant. For example, the default type for the integer 4
is int
, and the default type for the floating-point number 3.14
is float64
.
Therefore, because the result type of the switch
expression in the above code is int
, while the result types of the sub-expressions in the case
expressions are int8
, they are not the same type. Hence, this switch
statement cannot compile successfully.
Let’s take a look at a similar piece of code:
value2 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value2[4] {
case 0, 1:
fmt.Println("0 or 1")
case 2, 3:
fmt.Println("2 or 3")
case 4, 5, 6:
fmt.Println("4 or 5 or 6")
}
The values of variables value2
and value1
are exactly the same. However, what’s different is that I replaced the switch
expression with value2[4]
, and replaced the three case
expressions with case 0, 1
, case 2, 3
, and case 4, 5, 6
respectively.
As a result, the result value of the switch
expression is of type int8
, while the result values of the subexpressions in the case
expressions are untyped constants. This is the opposite of the previous situation. Can this switch
statement be compiled?
The answer is yes. Because if the result value of the subexpression in the case
expression is an untyped constant, its type will be automatically converted to the result type of the switch
expression. Moreover, since the aforementioned integers can all be converted to the value of type int8
, there is no problem in performing equality comparison on these expressions.
Of course, if the automatic conversion mentioned here fails, the switch
statement still cannot be compiled.
(Automatic type conversion in switch statements)
Through these two questions, you should be able to understand the relationship between the switch
expression and the case
expression. Since an equality comparison is needed, the result types of the subexpressions in both of them need to be the same.
The switch
statement performs limited type conversions, but it cannot guarantee that these conversions can unify their types. Also, if the result types of these expressions are of any interface type, you must be cautious in checking whether their dynamic values allow comparability (or equality comparisons).
Because if the answer is negative, although it will not cause a compilation error, the consequences will be more serious: it will trigger a panic (that is, a runtime panic).
Question 2: What are the constraints on the case
expressions in a switch
statement?
In the explanation of the previous question, I emphasized a point. I don’t know if you noticed it. That is: the switch
statement has uniqueness in the selection of case
clauses.
Because of this, the switch
statement does not allow the subexpressions of the case
expressions to have equal result values, regardless of whether these result values of the subexpressions exist in different case
expressions. Please refer to this code:
value3 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value3[4] {
case 0, 1, 2:
fmt.Println("0 or 1 or 2")
case 2, 3, 4:
fmt.Println("2 or 3 or 4")
case 4, 5, 6:
fmt.Println("4 or 5 or 6")
}
The value of the variable value3
is the same as value1
, which is an array composed of 7 integers from 0
to 6
. The element type is int8
. The switch
expression is value3[4]
, and the three case
expressions are case 0, 1, 2
, case 2, 3, 4
, and case 4, 5, 6
.
Since there are subexpressions in these three case
expressions with equal result values, this switch
statement cannot be compiled. However, fortunately, this constraint itself has another constraint, which is only targeted at subexpressions with result values that are constants.
For example, the subexpressions 1+1
and 2
cannot both appear, and 1+3
and 4
cannot both appear. With this constraint, we can find a way to bypass the restriction on subexpressions. Let’s take a look at another code snippet:
value5 := [...]int8{0, 1, 2, 3, 4, 5, 6}
switch value5[4] {
case value5[0], value5[1], value5[2]:
fmt.Println("0 or 1 or 2")
case value5[2], value5[3], value5[4]:
fmt.Println("2 or 3 or 4")
case value5[4], value5[5], value5[6]:
fmt.Println("4 or 5 or 6")
}
The variable name has been changed to value5
, but that’s not the focus. The focus is that I have replaced all the constants in the case
expressions with index expressions like value5[0]
.
Although the first case
expression and the second case
expression both contain value5[2]
, and the second case
expression and the third case
expression both contain value5[4]
, this is no longer a problem. This switch
statement can be compiled successfully.
However, this bypass method does not work for type switch statements. This is because the sub-expressions in the case
expressions of type switch statements must be directly represented by type literals and cannot be represented indirectly. The code is as follows:
value6 := interface{}(byte(127))
switch t := value6.(type) {
case uint8, uint16:
fmt.Println("uint8 or uint16")
case byte:
fmt.Printf("byte")
default:
fmt.Printf("unsupported type: %T", t)
}
The value of the variable value6
is of the empty interface type. It wraps a value of type byte
with a value of 127
. I use a type switch statement to determine the actual type of value6
and print the corresponding content.
There are two regular case
clauses and one default case
clause. The case
expressions of the former are case uint8, uint16
and case byte
. Do you remember? The byte
type is an alias type of uint8
type.
Therefore, they are essentially the same type, only with different type names. In this case, this type switch statement cannot be compiled because the sub-expressions byte
and uint8
are duplicated. Okay, that’s it for the constraints and workarounds of case
expressions. Did you learn?
Summary
Today we mainly discussed for
statements and switch
statements, but I didn’t explain the grammar rules because they are too simple. What we need to pay more attention to are the details hidden in the Go language specification and best practices.
These details are what many technical beginners call “pitfalls”. For example, when I talked about for
statements, I explained what it means to have only one iteration variable when carrying a range
clause. You must know that if there is only one iteration variable when iterating over an array or a slice, you cannot iterate over its element values, otherwise your program may not run as expected.
Also, the result values of the range
expression are copied and the original values are not used during actual iteration. What this affects depends on whether the result value is of value type or reference type.
Speaking of switch
statements, you need to understand that all the result values of the sub-expressions in the case
expressions need to be compared with the result value of the switch
expression, so their types must be the same or can be unified to the result type of the switch
expression. If not, this switch
statement cannot be compiled.
Finally, the result values of all the sub-expressions in the case
expressions of the same switch
statement cannot be duplicated, but luckily, this only applies to sub-expressions directly represented by literals.
Please remember that the order in which regular case
clauses are written is important, and the sub-expressions in the uppermost case
clause are always evaluated first and compared in this order. Therefore, if some sub-expressions have duplicate result values and they are equal to the result value of the switch
expression, the upper case
clause will always be selected.
Thinking Exercise
- In a type switch statement, how do we perform the corresponding type conversion on the value that was judged to be of a certain type?
- In an
if
statement, what is the scope of the variables declared in the initialization clause?
Click here to view the detailed code accompanying the Go Language column article.