"Go Language Lesson 1" Course Study Notes (11)

control structure

The "happy path" principle of if

  • For the branch structure of the program, Go provides two statement forms of if and switch-case; for the loop structure, Go only reserves the for loop statement form.

if statement

  • The if statement is a branch control structure provided in the Go language, and it is also the most commonly used and simplest branch control structure in Go. It will choose one of the two branches to execute according to the value of the Boolean expression.
  • Although almost all programming languages ​​support if statements natively, Go's if statements still have their own characteristics:
    • First, like a Go function, the opening brace of the branch code block of the if statement is on the same line as the if keyword, which is also a unified requirement of the Go code style, and the gofmt tool will help us achieve this;
    • Second, the whole Boolean expression of the if statement does not need to be wrapped in parentheses , which reduces the number of keystrokes for developers to a certain extent. Moreover, the evaluation result of the conditional judgment expression after the if keyword must be a Boolean type , that is, either true or false.
    • If there are many judgment conditions, we can use multiple logical operators to connect multiple conditional judgment expressions. For example, this code uses multiple logical operators && to connect multiple Boolean expressions:
      if (runtime.GOOS == "linux") && (runtime.GOARCH == "amd64") &&
      (runtime.Compiler != "gccgo") {
              
              
      	println("we are using standard go compiler on linux os for amd64")
      }
      
      insert image description here
  • There are other forms of if statement in Go language, such as two-branch structure and multi-(N) branch structure. The multi-branch structure introduces else if.

Support for declaring self-use variables in if statements

  • Whether it is a single-branch, two-branch or multi-branch structure, we can declare some variables before the Boolean expression after if, and the variables declared before the if Boolean expression are called self-use variables of the if statement. As the name implies, these variables can only be used within the code block scope of the if statement.
  • Declaring self-use variables in an if statement is a common usage in the Go language. This usage can intuitively make developers feel that the number of lines of code is reduced and improve readability. At the same time, since these variables are self-used variables of the if statement, their scope is limited to the implicit code blocks of each layer of the if statement, and these variables cannot be accessed and changed outside the if statement, which makes these variables have a certain isolation, so you It can also be more focused when reading and understanding the code of the if statement.

The "Happy Path" Principle of if Statements

  • From the perspective of readability, the single-branch structure is better than the two-branch structure, and the two-branch structure is better than the multi-branch structure. Obviously, we need to reduce the use of multi-branch structures or even two-branch structures in our daily coding, which will help us write elegant, concise, easy-to-read, easy-to-maintain, and error-prone code.
  • The "Happy Path" principle of if statements
    • Use only single-branch control structures;
    • Fast return in a single branch when a Boolean expression evaluates to false, that is, when an error occurs;
    • The normal logic is always "to the left" in the code layout, so that readers can see the whole picture of the function's normal logic at a glance from top to bottom;
    • Function execution to the last line represents a successful state.

switch statement in Go

Know the switch statement

  • In addition to the if statement, the Go language also provides a branch control structure that is more suitable for multi-way branch execution, that is, the switch statement.
  • In some scenarios with many execution branches, using the switch branch control statement can make the code more concise and readable.
  • The general form of switch statement in Go language:
    switch initStmt; expr {
          
          
    	case expr1:
    	// 执行分支1
    	case expr2:
    	// 执行分支2
    	case expr3_1, expr3_2, expr3_3:
    	// 执行分支3
    	case expr4:
    	// 执行分支4
    	... ...
    	case exprN:
    	// 执行分支N
    	default:
    	// 执行默认分支
    }
    
    • First look at the first line in the general form of the switch statement. This line starts with the switch keyword, which is usually followed by an expression (expr). The initStmt in this sentence is an optional component.
    • We can define some temporary variables used in the switch statement through short variable declarations in initStmt.
    • Next, the curly braces behind the switch are code execution branches one by one. Each branch starts with the case keyword, and each case is followed by an expression or a comma-separated expression list.
    • There is also a special branch starting with the default keyword, called the default branch.
    • Finally, let's look at the execution flow of the switch statement. The switch statement will use the evaluation result of expr to compare with the expression results in each case. If a matching case is found, that is, the expression after the case, or the evaluation result of any expression in the expression list and the expression result of expr If the evaluation results are the same, the code branch corresponding to the case will be executed. After the branch is executed, the switch statement will end.
    • If none of the case expressions can match expr, the program executes the default branch and ends the switch statement.
    • Go first evaluates the switch expr expression, and then evaluates the case statements one by one from top to bottom in the order in which they appear. In a case statement with a list of expressions, Go evaluates the expressions in the list from left to right.

The flexibility of the switch statement

  • The switch statement in C language has restrictions on the expression type, and each case statement can only have one expression. Also, unless you explicitly use break to jump out, the program always executes the next case statement by default. These feature developers bring a mental burden to use.
  • Compared with the "rigidity" of the switch statement in C language, Go's switch statement shows great flexibility, mainly in the following aspects:
    • First of all, the evaluation result of each expression in the switch statement can be a value of various types, as long as its type supports comparison operations.
      • In the C language, the evaluation results of all expressions used in the switch statement can only be int or enumeration types, and other types will be rejected by the C compiler.
      • The Go language is much more tolerant, as long as the type supports comparison operations, it can be used as the expression type in the switch statement. For example, integer, Boolean, string, complex, and element types are all comparable array types, and even field types are all comparable structure types.
        type person struct {
                  
                  
        	name string
        	age int
        } 
        func main() {
                  
                  
        	p := person{
                  
                  "tom", 13}
        	switch p {
                  
                  
        	case person{
                  
                  "tony", 33}:
        		println("match tony")
        	case person{
                  
                  "tom", 13}:
        		println("match tom")
        	case person{
                  
                  "lucy", 23}:
        		println("match lucy")
        	default:
        		println("no match")
        	}
        }
        
    • Second point: The switch statement supports the declaration of temporary variables.
    • The third point: the case statement supports a list of expressions.
    • Fourth point: Cancel the semantics of executing the next case code logic by default.

type switch

  • "type switch" This is a special use of the switch statement:
    func main() {
          
          
    	var x interface{
          
          } = 13
    	switch x.(type) {
          
          
    	case nil:
    		println("x is nil")
    	case int:
    		println("the type of x is int")
    	case string:
    		println("the type of x is string")
    	case bool:
    		println("the type of x is string")
    	default:
    		println("don't support the type")
    	}
    }
    
    • The expression following the switch keyword is x.(type), this expression form is exclusive to the switch statement and can only be used in the switch statement. x in this expression must be an interface type variable, and the evaluation result of the expression is the dynamic type corresponding to this interface type variable.
    • Then, what follows the case keyword is not an expression in the ordinary sense, but a specific type. In this way, Go can use the dynamic type of the variable x to match the type in each case, and the subsequent logic is the same.
  • The Go language specification clearly stipulates that the break statement without a label interrupts the execution and jumps out of the innermost for, switch or select in the same function where the break statement is located.

Go's for loop

Classic usage of the for statement

  • All mainstream programming languages ​​provide support for loop structures. Most mainstream languages, including C, C++, Java, and Rust, and even the dynamic language Python provide more than one loop statement, but Go has only one. That is the for statement.
  • The classic form of the for loop statement in Go language:
    var sum int
    for i := 0; i < 10; i++ {
          
          
    	sum += i
    }
    println(sum)
    

insert image description here

  • The component corresponding to ① in the figure is executed before the loop body (③), and will only be executed once in the entire for loop statement, which is also called the loop preceding statement.
    • We usually declare some self-use variables used in the loop body (③) or loop control conditions (②) in this part, also called loop variables or iteration variables, such as the integer variable i declared here.
    • Like the self-use variables in the if statement, the for loop variable also adopts the form of short variable declaration, and the scope of the loop variable is limited to the implicit code block of the for statement.
  • The component corresponding to ② in the figure is a conditional judgment expression used to determine whether the cycle should continue.
    • Like the if statement, the expression used for conditional judgment must be a Boolean expression. If there are multiple judgment conditions, we can also connect them with logical operators.
    • When the evaluation result of the expression is true, the code will enter the loop body (③) to continue execution, otherwise, the loop will end directly, and neither the loop body (③) nor the component ④ will be executed.
  • The component corresponding to ③ in the figure is the loop body of the for loop statement.
    • If the relevant judgment condition expression evaluation structure is true, the loop body will be executed once, and such an execution is also called an iteration (Iteration).
    • In the above example, the action performed by the loop body is to accumulate the value of the variable i in this iteration into the variable sum.
  • The components corresponding to ④ in the figure will be executed after each iteration of the loop body, which is also called the post-loop statement.
    • This part is usually used to update the loop variable declared in part ① of the for loop statement. For example, in this example, we add 1 to the loop variable i in this component.
  • The for loop of Go language supports the declaration of multiple loop variables, and can be applied in the loop body and judgment conditions:
    for i, j, k := 0, 1, 2; (i < 20) && (j < 10) && (k < 30); i, j, k = i+1, j+1,
    	sum += (i + j + k)
    	println(sum)
    }
    
    • Except for the loop body part, the remaining three parts are optional.
    • Although the pre-statement or post-statement is omitted, the semicolon in the classic for loop form is still preserved, which is a requirement of the Go syntax.
    • When the pre- and post-statements of the loop are omitted, and only the conditional expression of the loop is reserved, we can omit the semicolon in the classic for loop form.
    • When the evaluation result of the loop judgment condition expression of the for loop statement is always true, we can omit it. This for loop is what we usually call an "infinite loop".

for range loop form

  • For complex data types like slices and Go's native string type (string), Go language provides a more convenient "syntactic sugar" form: for range.
    var sl = []int{
          
          1, 2, 3, 4, 5}
    for i, v := range sl {
          
          
    	fmt.Printf("sl[%d] = %d\n", i, v)
    }
    
    • The form of the for range loop is quite different from the classic form of the for statement. Except for the loop body, the rest of the components are "disappeared". In fact, those parts have been integrated into the semantics of for range.
    • The i and v here correspond to the loop variables of the loop preceding statement in the classic for statement form, and their initial values ​​are the subscript value and element value of the first element of the slice sl respectively.
    • The loop control condition implicit in the semantics of for range is judged as: whether all the elements of sl have been traversed, which is equivalent to the Boolean expression i < len(sl).
    • After each iteration, for range will fetch the subscript and value of the next element of the slice sl, and assign them to the loop variables i and v respectively, which is the same logic as the loop post statement in the classic form of for.
  • The for range statement also has several common "variants":
    • Variation 1: When we don't care about the value of the element, we can omit the variable v representing the element value, and only declare the variable i representing the subscript value.
    • Variation 2: If we don't care about the element subscript, but only care about the element value, then we can replace the variable i representing the subscript value with an empty identifier. It must be noted here that this empty identifier cannot be omitted, otherwise it will be the same as the above "variant 1" form, and the Go compiler will not be able to distinguish it.
    • Variation 3: If we don't care about the subscript value or the element value, we can omit both i and v.

string type

  • for range For the string type, the v value obtained by each cycle is a Unicode character code point, that is, a rune type value, not a byte, and the first returned value i is the memory code of the Unicode character code point (UTF-8) position of the first byte in the memory sequence of the string.
  • The semantics of looping over a string type are different when using the for classic form than when using the for range form.

map

  • A map is a collection of key-value pairs (key-value). The most common operation on a map is to obtain its corresponding value through the key. But sometimes, we also need to traverse the map collection, which requires the support of the for statement.
  • But in the Go language, we need to perform a loop operation on the map, for range is the only way, the classic for loop form does not support the loop control of the map type variable.
  • for range For the map type, the loop variables k and v will be assigned as the key value and value value of an element in the map key-value pair collection for each loop.

channel

  • Channel is a primitive of concurrency design provided by Go language, which is used for communication between multiple Goroutines.
  • When the channel type variable is used as the iteration object of the for range statement, the for range will try to read data from the channel, and the usage form is as follows:
    var c = make(chan int)
    for v := range c {
          
          
    	// ...
    }
    
    • Each time for range reads an element from the channel, it will assign it to the loop variable v and enter the loop body. When there is no data to read in the channel, the for range loop will block on the read operation on the channel.
    • The for range loop will not end until the channel is closed, which is also the implicit loop judgment condition when the for range loop cooperates with the channel.

continue statement with label

  • In daily development, due to the needs of algorithm logic, we may interrupt the current loop body and continue to the next iteration, or interrupt the loop body and completely end the loop statement. For these situations, Go language provides continue statement and break statement.
    • If the code in the loop body is executed halfway, it is necessary to interrupt the current iteration, ignore the subsequent code in the iteration loop body, and return to the for loop condition judgment to try to start the next iteration. At this time, we can use the continue statement to deal with it.
    • The continue in the Go language adds support for labels on the basis of the continue semantics in the C language.
      • The role of the label statement is to mark the target of the jump.
        func main() {
                  
                  
        	var sum int
        	var sl = []int{
                  
                  1, 2, 3, 4, 5, 6}
        loop:
        	for i := 0; i < len(sl); i++ {
                  
                  
        		if sl[i]%2 == 0 {
                  
                  
        		// 忽略切片中值为偶数的元素
        		continue loop
        	}
        		sum += sl[i]
        	}
        	println(sum) // 9
        }
        
      • In this code, we define a label: loop, and the jump target it marks is exactly our for loop. That is to say, we can use continue+ loop label in the loop body to realize loop body interruption.
      • Usually we directly use the continue statement without label in such a non-nested loop scenario.
      • A continue statement with a label, usually found in nested loop statements, is used to jump to the outer loop and continue execution to the next iteration of the outer loop statement:
        func main() {
                  
                  
        	var sl = [][]int{
                  
                  
        		{
                  
                  1, 34, 26, 35, 78},
        		{
                  
                  3, 45, 13, 24, 99},
        		{
                  
                  101, 13, 38, 7, 127},
        		{
                  
                  54, 27, 40, 83, 81},
        	}
        outerloop:
        	for i := 0; i < len(sl); i++ {
                  
                  
        		for j := 0; j < len(sl[i]); j++ {
                  
                  
        			if sl[i][j] == 13 {
                  
                  
        				fmt.Printf("found 13 at [%d, %d]\n", i, j)
        				continue outerloop
        			}
        		}
        	}
        }
        

Use of the break statement

  • Whether with or without label, the essence of the continue statement is to continue the execution of the loop statement. But in daily coding, we will also encounter some scenarios. In these scenarios, we not only need to interrupt the iteration of the current loop body, but also completely jump out of the loop at the same time, terminating the execution of the entire loop statement. Faced with such a scenario, the continue statement is no longer applicable, and the Go language provides us with a break statement to solve this problem.
  • Like the continue statement, Go also adds support for labels to the break statement. Moreover, like the continue statement, if a nested loop is encountered, it is not enough to use a break without a label if a break wants to jump out of the outer loop, because a break without a label can only jump out of the innermost loop where it is located. In order to realize the jump out of the outer loop, we also need to add a label to the break.

Common "pitfalls" and avoidance methods of for statements

  • Problem 1: Reuse of loop variables
    • The loop statement in the form of for range uses short variable declarations to declare loop variables. The loop body will use these loop variables to implement specific logic, but when using them, it may be found that the value of the loop variable does not match the "expected".
    • Sometimes loop variables are declared only once in a for range statement and are reused on each iteration.
  • Question 2: Participating in the loop is a copy of the range expression
    • In the for range statement, the type of expression accepted by range can be array, pointer to array, slice, string, map and channel (with read permission).
      func main() {
              
              
      	var a = [5]int{
              
              1, 2, 3, 4, 5}
      	var r [5]int
      	fmt.Println("original a =", a)
      	for i, v := range a {
              
              
      	  if i == 0 {
              
              
      		a[1] = 12
      		a[2] = 13
      	  }
      	  r[i] = v
      	} 
      	fmt.Println("after for range loop, r =", r)
      	fmt.Println("after for range loop, a =", a)
      }
      
      • Each iteration is the element obtained from the value copy a' of the array a.
      • a' is a continuous byte sequence temporarily allocated by Go, which is completely different from a memory area.
      • Therefore, no matter how a is modified, its copy a' participating in the cycle still maintains the original value, so what v takes out from a' is still the original value of a, not the modified value.
    • So how to solve this problem so that the output results meet our expectations?
      • Most of the scenarios where arrays are used can be replaced by slices:
        func main() {
                  
                  
        	var a = [5]int{
                  
                  1, 2, 3, 4, 5}
        	var r [5]int
        	fmt.Println("original a =", a)
        	for i, v := range a[:] {
                  
                  
        		if i == 0 {
                  
                  
        			a[1] = 12
        			a[2] = 13
        		}
        		r[i] = v
        	} 
        	fmt.Println("after for range loop, r =", r)
        	fmt.Println("after for range loop, a =", a)
        }
        
      • In the range expression, we use a[:] to replace the original a, that is, the array a is converted into a slice, which is used as the loop object of the range expression.
      • When copying a range expression, what we actually copy is a slice, that is, a structure representing a slice. The array in the structure representing the copy of the slice still points to the underlying array corresponding to the original slice, so our modifications to the copy of the slice will also be reflected in the underlying array a.
      • And v obtains the array element from the underlying array pointed to by array in the slice copy structure, and obtains the modified element value.
  • Question 3: Traversing the randomness of elements in the map
    • If we modify the map during the loop, will the result of such modification affect subsequent iterations? This result is as random as we traverse the map.
    • When our daily coding encounters scenarios where the map needs to be modified while traversing the map, we must be extra careful.

Guess you like

Origin blog.csdn.net/fangzhan666/article/details/132448868