Lesson 5/41 · Go Language Intro & Dev Environment Beginner m01-l05

Operators, Control Flow and Scope

Module 1: Go Language Intro & Dev Environment | Lesson: m01-l05 | Level: Beginner After this lesson you will be able to: use Go's distinctive control-flow syntax, understand variable scope and shadowing, and write idiomatic Go control-flow code.

Learning Objectives

  • Use Go's arithmetic, comparison, logical and bitwise operators correctly, and understand their precedence rules.
  • Skillfully write control flow using if initialization statements, the four forms of for, and the features of switch.
  • Explain the concepts of variable scope and shadowing, and avoid common shadowing pitfalls.
  • Recognize and fix compile errors or logic bugs caused by scope or operator precedence.

Core Concepts

1.1 Go Operators and Precedence

Go's operators fall into arithmetic, comparison, logical, bitwise and assignment operators. Understanding operator precedence is essential for writing unambiguous expressions.

1. Arithmetic and comparison operators These include +, -, *, /, % and ==, !=, <, <=, >, >=. They are mostly the same as in other languages.

2. Logical operators These include && (logical and) and || (logical or). Go supports short-circuit evaluation: - a && b: if a is false, b is not evaluated. - a || b: if a is true, b is not evaluated. This is very useful for guarding against nil pointer dereference or invalid-argument checks.

3. Bitwise operators Go supports bit operations on integer types: - & (bitwise AND): a & b - | (bitwise OR): a | b - ^ (bitwise XOR / complement): a ^ b is XOR; ^a is the unary complement. - &^ (AND NOT): a &^ b clears from a the bits that are 1 in b. This is a Go-specific operator, often used for masking. - <<, >> (left shift / right shift)

4. Precedence Go's operator precedence, from highest to lowest, is as follows (operators of equal precedence associate left to right, except for right shift):

Precedence Operators Meaning
5 * / % << >> & &^ multiplicative / shift / bitwise AND
4 + - | ^ additive / bitwise OR / bitwise XOR
3 == != < <= > >= comparison
2 && logical AND
1 || logical OR

Note: the unary operators +, -, !, ^, *, &, <- have the highest precedence (level 6). ++ and -- are statements rather than operators and have no precedence.

1.2 if with an Initialization Statement

Go's if statement supports an optional initialization statement that runs before the if is evaluated and whose scope is limited to the if block. This helps reduce variable pollution and makes the code more compact.

if err := doSomething(); err != nil {
    // err is only valid in this scope
    log.Fatal(err)
}
// err is not visible here

1.3 The Four Forms of for

Go has only one loop construct: for. Through different forms it can emulate the while, do-while and foreach of other languages.

  1. Standard loop (C style) go for init; condition; post { } // for example: for i := 0; i < 5; i++ { }
  2. Condition-only loop (while style) go for condition { } // for example: for i < 5 { i++ }
  3. Infinite loop go for { } // usually used together with break
  4. for range loop Used to iterate over arrays, slices, strings, maps, channels, and (since Go 1.22) integers. go for index, value := range slice { }
  5. Emulating do-while Go has no direct do-while, but you can emulate it like this: go for { // loop body if !condition { break } }

Go 1.22 change: starting with Go 1.22 (in effect based on the go directive in the module's go.mod), for loops (both the three-clause form and for range) create a fresh loop variable on each iteration. This means capturing the loop variable inside a goroutine no longer needs extra closure tricks; each goroutine gets its own copy of the variable.

1.4 switch without break and Type Switches

Go's switch statement is very powerful: 1. No break needed: each case automatically falls through to the end after it finishes, unless you use fallthrough. 2. fallthrough: unconditionally executes the statement body of the next case. It easily obscures intent, so use it only when you genuinely need fallthrough. 3. Type compatibility check: the type of the expression after switch must be compatible (or convertible) with the types of the case values. 4. Type switch: used to inspect the dynamic type of an interface. go switch x.(type) { case int: // ... case string: // ... }

1.5 Scope and Variable Shadowing

Scope determines where an identifier (variable, function name, etc.) is visible in the code. Go's scopes include: - Universe scope: predeclared identifiers (such as int, nil, true). - Package scope: identifiers declared outside functions, under the package declaration; visible to all files in the same package. - File scope: package names brought in by import, visible only in the current file. - Block scope: a block enclosed by {}, such as the inside of if, for or switch.

Shadowing: when an inner scope declares a variable with the same name as one in an outer scope, the inner variable "shadows" the outer one. Inside the inner scope, referring to that name resolves to the inner variable. This can cause bugs, because the outer variable's value is not actually modified.

Example Code

Example 1: Bitwise Operators and for range Basics

This example demonstrates bitwise operators (including &^ and the unary ^), logical short-circuiting, and the basic use of for range.

package main

import "fmt"

func main() {
    a := uint8(12) // 00001100
    b := uint8(10) // 00001010

    // Bitwise demo
    // ^a is the unary complement; for uint8 it is 255 - 12 = 243 (11110011)
    fmt.Printf("^a    = %08b (%d)\n", ^a, ^a)

    // a &^ b is AND NOT, clearing the bits set in b: 00001100 &^ 00001010 = 00000100
    fmt.Printf("a &^ b = %08b (%d)\n", a&^b, a&^b)

    // Logical short-circuit demo
    // If a > 10 were false, a < 20 would not be evaluated.
    if a > 10 && a < 20 {
        fmt.Println("Short-circuit OR worked")
    } else {
        fmt.Println("Condition failed")
    }

    // for range demo
    slice := []int{1, 2, 3}
    for i, v := range slice {
        fmt.Printf("Index: %d, Value: %d\n", i, v)
    }
}

Expected output:

^a    = 11110011 (243)
a &^ b = 00000100 (4)
Condition failed
Index: 0, Value: 1
Index: 1, Value: 2
Index: 2, Value: 3

Example 2: switch Features and Scope Shadowing

This example demonstrates expression matching in switch, the if initialization statement, and the potential problems caused by variable shadowing.

package main

import "fmt"

func main() {
    // 1. switch expression matching
    score := 85
    switch {
    case score >= 90:
        fmt.Println("A")
    case score >= 80:
        fmt.Println("B")
    default:
        fmt.Println("C")
    }

    // 2. if initialization statement and scope
    // val is only visible inside the if block
    if val := 10; val > 5 {
        fmt.Println("val inside if:", val)
    }
    // fmt.Println(val) // error: val is undefined

    // 3. A variable shadowing example
    err := outerError()
    fmt.Println("Outer err:", err)

    // An inner err with the same name shadows the outer one
    if innerErr := innerError(); innerErr != nil {
        fmt.Println("Inner err:", innerErr)
        // The err here is innerErr, not the outer err
    }
    // The outer err is still "outer error"
    fmt.Println("Outer err after if:", err)
}

func outerError() error {
    return fmt.Errorf("outer error")
}

func innerError() error {
    return fmt.Errorf("inner error")
}

Expected output:

B
val inside if: 10
Outer err: outer error
Inner err: inner error
Outer err after if: outer error

Common Mistakes & Pitfalls

  • Wrong assumption: ^ is always bitwise XOR
  • Why: ^ is both a unary operator (bitwise complement) and a binary operator (bitwise XOR). In the expression ^a & b, because unary operators have the highest precedence, it is equivalent to (^a) & b, not ^(a & b).
  • Correct approach: make your intent explicit and use parentheses when needed. For example, ^(a & b) means AND first, then complement.

  • Wrong code: switch case type mismatch

  • Why: in switch num, num is an int, but the cases write boolean expressions (such as num > 0). Go requires the values after case to be type-comparable with the switch expression.
  • Correct approach: if you need boolean expressions, use switch { case num > 0: ... } (equivalent to switch true).

  • Wrong code: variable shadowing causes a logic error

  • Why: declaring a variable with the same name in an inner scope means the outer variable is not updated.
  • Correct approach: avoid redeclaring a same-named variable in an inner scope, or use a different variable name. For example: go var err error if err = doSomething(); err != nil { // Use assignment, not a short variable declaration }

Best Practices

  • Use if initialization statements: declare temporary variables in if to limit their scope and reduce naming pollution.
  • Prefer for range: when iterating over a collection, for range is safer and more concise than an index loop.
  • Use switch instead of nested if-else: when you have multiple discrete value checks, switch reads better and states intent more clearly; in most cases the performance difference is negligible, so do not use it as a selection criterion.
  • Know the &^ operator: use &^ when you need to clear specific bits; it is more readable and less error-prone than & ^.
  • Go 1.22+ loop variables: if you use Go 1.22 or later, you need not worry about capturing loop variables in goroutines; if you must support older versions, create a local copy inside the loop body.

Exercises

  1. Concept question: explain what fallthrough does in Go and what its potential risks are.
  2. Coding exercise: write a function classifyNumber(n int) string that uses a switch statement to determine whether n is "positive", "negative" or "zero".
  3. Advanced/debugging exercise: the code below is meant to print 0 through 4, but often prints nothing or behaves oddly. Find the problem and fix it. ```go package main

    import ( "fmt" "sync" )

    func main() { var wg sync.WaitGroup for i := 0; i < 5; i++ { wg.Add(1) go func() { fmt.Println(i) wg.Done() }() } } ```

Reference Answers

Reference Answers (click to expand / collapse)

Question 1

fallthrough is a keyword that was discouraged before Go 1.23 and is still retained from Go 1.23 onward but must be used carefully. It unconditionally executes the statement body of the next case, easily obscuring intent; use it only when you genuinely need fallthrough.

Question 2

package main

import "fmt"

func classifyNumber(n int) string {
    switch {
    case n > 0:
        return "positive"
    case n < 0:
        return "negative"
    default:
        return "zero"
    }
}

func main() {
    fmt.Println(classifyNumber(10)) // positive
    fmt.Println(classifyNumber(-5)) // negative
    fmt.Println(classifyNumber(0))  // zero
}

Question 3

Problem analysis: 1. The given code lacks wg.Wait(), so main exits immediately, terminating the goroutines before they run — which is why it often prints nothing. 2. In Go 1.21 and earlier, the for loop variable i is the same variable across iterations, so all goroutines capture the same address and may eventually print the same value (usually 4). But in Go 1.22+, each iteration creates a new variable, fixing this automatically. 3. To be compatible with older versions and ensure correctness, make a copy of i inside the goroutine.

Fixed code:

package main

import (
    "fmt"
    "sync"
)

func main() {
    var wg sync.WaitGroup
    for i := 0; i < 5; i++ {
        wg.Add(1)
        // Make a local copy to avoid the loop-variable capture issue in Go 1.21 and earlier
        val := i
        go func() {
            fmt.Println(val)
            wg.Done()
        }()
    }
    // Wait for all goroutines to finish
    wg.Wait()
}

Lesson Summary

  • You have mastered Go's operator precedence, especially the difference between the bitwise &^ and the unary ^.
  • You can skillfully use if initialization statements, the four forms of for, and for range.
  • You understand that switch needs no break, and when to use fallthrough.
  • You are clear on variable scope and shadowing, and can avoid common naming-conflict bugs.

Next lesson preview: functions — multiple return values, named return values and variadic parameters.

Feedback

This site is front-end only — no backend, no accounts — so feedback goes to GitHub Issues, Discussions, or email.

Paste it into the issue or email to help track it down — it never includes anything you typed

Also:Browse existing issues · Email feedback (no GitHub account needed):278975598@qq.com