18 if Statements, for Loops, and Switch Statements

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 file demo41.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 ith element value of numbers1, and assign the result as the new ith 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:

  1. The range expression is only evaluated once when the for statement begins execution, regardless of how many iterations there will be afterwards.
  2. The result of evaluating the range expression is copied. In other words, the object being iterated is a copy of the result value of the range 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.

Switch statement with automatic type conversion

(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

  1. 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?
  2. 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.