Files
test/chapters/chapter-2-basics.md

19 KiB
Raw Permalink Blame History

第二章Go 基础语法 —— 变量、类型与流程控制

本章目标:深入理解 Go 的变量声明机制、核心数据类型、运算符体系及流程控制结构,掌握 Go 独特的"零值"哲学和"类型推断"特性。

2.1 Go 的变量声明机制

Go 语言的变量声明方式灵活多样,理解其背后的机制是编写高效代码的基础。

2.1.1 四种声明方式

1. var 声明(显式类型)

var name string = "Alice"
var age int = 25
var isActive bool = true

深度解析

  • var 是 Go 中最正式的声明方式
  • 可以省略初始化,此时变量会被赋予零值(见 2.2 节)
  • 支持在函数内外声明(包级变量和局部变量)

2. var 声明(类型推断)

var name = "Bob"      // 编译器推断为 string
var count = 100       // 编译器推断为 int
var ratio = 3.14      // 编译器推断为 float64

深度解析

  • 当提供初始值时Go 编译器会自动推断类型
  • 这是推荐的声明方式,代码更简洁
  • 类型一旦推断不可更改Go 是静态类型语言)

3. 短变量声明 :=(最常用)

name := "Charlie"
age := 30
isValid := true

深度解析

  • :=var + 类型推断的语法糖
  • 只能在函数内部使用(不能在包级作用域使用)
  • 如果变量已存在,会重新赋值而非声明新变量
  • 可以混合新旧变量:a, b := 1, 2(即使 a 已存在)
// 错误示例:包级作用域不能使用 :=
package main

// x := 10  // 编译错误!

func main() {
    y := 20  // 正确
}

4. 多变量声明

// 显式类型
var (
    name string = "David"
    age  int    = 28
)

// 类型推断
var (
    city  = "Beijing"
    score = 95.5
)

// 短声明
x, y, z := 1, 2, 3

深度解析

  • 使用 var () 块可以批量声明变量,适合包级变量
  • 短声明 := 支持多个变量同时声明
  • 注意:= 左侧至少有一个变量是新的,否则是赋值操作
a, b := 1, 2      // 声明两个新变量
a, c := 3, 4      // a 已存在c 是新变量(合法)
a, b := 5, 6      // a 和 b 都已存在,编译错误!

2.1.2 变量作用域

Go 的作用域规则非常清晰:

package main

import "fmt"

// 包级变量(全局作用域)
var globalVar = "I am global"

func main() {
    // 函数级变量
    localVar := "I am local"
    
    if true {
        // 块级变量if 块内)
        blockVar := "I am in if block"
        fmt.Println(blockVar)
    }
    
    // fmt.Println(blockVar)  // 编译错误blockVar 在 if 块外不可见
    
    fmt.Println(localVar)
    fmt.Println(globalVar)
}

深度解析

  • 包级作用域:在 package 声明后,任何函数外声明的变量
  • 函数作用域:在函数内部声明的变量
  • 块级作用域:在 {} 代码块内声明的变量if、for、switch 等)
  • 内层作用域可以遮蔽外层同名变量
func shadowExample() {
    x := 10
    if true {
        x := 20  // 创建了新变量 x遮蔽了外层的 x
        fmt.Println(x)  // 输出 20
    }
    fmt.Println(x)  // 输出 10外层 x 未受影响)
}

2.2 Go 的"零值"哲学

Go 语言有一个非常优雅的设计:未初始化的变量会自动获得零值

2.2.1 零值表

类型 零值 说明
int, int8, int16, int32, int64 0 整数类型
uint, uint8, uint16, uint32, uint64 0 无符号整数
float32, float64 0.0 浮点数
complex64, complex128 0 + 0i 复数
bool false 布尔值
string "" 空字符串
指针、函数、接口、切片、映射、通道 nil 引用类型
数组 数组元素全为零值 值类型

2.2.2 零值的实际意义

package main

import "fmt"

func main() {
    var intVar int
    var floatVar float64
    var boolVar bool
    var stringVar string
    var ptrVar *int
    
    fmt.Printf("int: %d\n", intVar)       // 0
    fmt.Printf("float: %f\n", floatVar)   // 0.000000
    fmt.Printf("bool: %v\n", boolVar)     // false
    fmt.Printf("string: '%s'\n", stringVar) // ''
    fmt.Printf("ptr: %v\n", ptrVar)       // <nil>
    
    // 零值判断
    if stringVar == "" {
        fmt.Println("字符串为空")
    }
    
    if ptrVar == nil {
        fmt.Println("指针为空")
    }
}

深度解析

  • 零值机制避免了"未初始化变量"的运行时错误
  • 在结构体中,未设置的字段自动为零值
  • 这是 Go 语言安全性的重要体现
type User struct {
    Name string
    Age  int
    Email string
}

func main() {
    u := User{}  // 所有字段为零值
    fmt.Printf("%+v\n", u)  // {Name: Age:0 Email:}
    
    // 零值判断
    if u.Name == "" {
        fmt.Println("用户未设置名称")
    }
}

2.3 核心数据类型详解

2.3.1 整型Integer Types

Go 提供了多种整型,适应不同场景:

var (
    a int8      // 8 位有符号整数,范围:-128 ~ 127
    b int16     // 16 位有符号整数,范围:-32768 ~ 32767
    c int32     // 32 位有符号整数,范围:-2147483648 ~ 2147483647
    d int64     // 64 位有符号整数,范围:-9223372036854775808 ~ 9223372036854775807
    
    e uint8     // 8 位无符号整数范围0 ~ 255
    f uint16    // 16 位无符号整数范围0 ~ 65535
    g uint32    // 32 位无符号整数范围0 ~ 4294967295
    h uint64    // 64 位无符号整数范围0 ~ 18446744073709551615
    
    i int       // 平台相关32 位系统为 int3264 位系统为 int64
    j uint      // 平台相关32 位系统为 uint3264 位系统为 uint64
)

深度解析

  • intuint 的大小取决于平台架构32 位或 64 位)
  • 优先使用 intint64,除非有特殊内存优化需求
  • byteuint8 的别名,常用于处理二进制数据
  • runeint32 的别名,用于表示 Unicode 字符
package main

import "fmt"

func main() {
    var b byte = 'A'      // byte 是 uint8 的别名
    var r rune = '中'     // rune 是 int32 的别名,可存储 Unicode 字符
    
    fmt.Printf("byte: %c, %d\n", b, b)  // A, 65
    fmt.Printf("rune: %c, %d\n", r, r)  // 中20013
    
    // 字节数测试
    fmt.Printf("len('A'): %d\n", len("A"))    // 1
    fmt.Printf("len('中'): %d\n", len("中"))  // 3UTF-8 编码占 3 字节)
}

2.3.2 浮点型Floating-Point Types

var (
    a float32 = 3.14
    b float64 = 3.141592653589793
)

深度解析

  • float64默认浮点类型,精度更高
  • float32 节省内存,适合对精度要求不高的场景(如图形处理)
  • 浮点数比较时注意精度问题
package main

import "fmt"
import "math"

func main() {
    a := 0.1 + 0.2
    b := 0.3
    
    fmt.Printf("a == b: %v\n", a == b)  // false精度问题
    fmt.Printf("a: %.20f\n", a)         // 0.30000000000000004441
    
    // 正确比较方式
    epsilon := 1e-9
    if math.Abs(a-b) < epsilon {
        fmt.Println("a 和 b 近似相等")
    }
}

2.3.3 复数类型Complex Types

var (
    c1 complex64  = 1 + 2i
    c2 complex128 = 3.5 + 4.5i
)

深度解析

  • ij 表示虚部
  • 较少使用,主要用于科学计算和信号处理

2.3.4 布尔类型Boolean Type

var (
    isTrue  bool = true
    isFalse bool = false
)

深度解析

  • 只有 truefalse 两个值
  • 不能与整数互相转换(0 不等于 false
  • 逻辑运算符:&&(与)、||(或)、!(非)
package main

import "fmt"

func main() {
    a, b := true, false
    
    fmt.Printf("a && b: %v\n", a && b)  // false
    fmt.Printf("a || b: %v\n", a || b)  // true
    fmt.Printf("!a: %v\n", !a)          // false
    
    // 短路求值
    if false && someExpensiveFunction() {
        // someExpensiveFunction() 不会被执行
    }
}

2.3.5 字符串类型String Type

var s1 string = "Hello"
var s2 string = "世界"
var s3 string = `多行
字符串`

深度解析

  • 字符串是不可变的字节序列UTF-8 编码)
  • 使用双引号 " 或反引号 `
  • 反引号表示原始字符串,不转义任何字符
  • 字符串长度使用 len(),但返回的是字节数而非字符数
package main

import "fmt"

func main() {
    s := "Hello 世界"
    
    fmt.Printf("len(s): %d\n", len(s))  // 11字节数
    fmt.Printf("s[0]: %c\n", s[0])      // H
    // fmt.Printf("s[6]: %c\n", s[6])   // 可能乱码(中文字符占 3 字节)
    
    // 遍历字符串(按 rune
    for i, r := range s {
        fmt.Printf("位置 %d: 字符 %c (Unicode: %d)\n", i, r, r)
    }
    
    // 字符串拼接
    s1 := "Hello"
    s2 := "World"
    s3 := s1 + " " + s2
    
    // 使用 strings.Builder高性能拼接
    var builder strings.Builder
    builder.WriteString("Hello")
    builder.WriteString(" ")
    builder.WriteString("World")
    fmt.Println(builder.String())
}

注意:需要导入 strings 包。

2.4 类型转换与类型推断

2.4.1 显式类型转换

Go 不支持隐式类型转换,必须显式转换:

package main

import "fmt"

func main() {
    var a int = 42
    var b float64 = float64(a)  // 显式转换
    var c int = int(b)          // 显式转换
    
    fmt.Printf("a: %d, type: %T\n", a, a)
    fmt.Printf("b: %f, type: %T\n", b, b)
    fmt.Printf("c: %d, type: %T\n", c, c)
    
    // 错误示例:隐式转换(编译错误)
    // var x int = 3.14  // 编译错误!
}

深度解析

  • 转换必须在兼容类型之间进行
  • 转换可能导致精度丢失溢出
  • 使用 int64(float64) 转换时注意范围检查
package main

import "fmt"
import "math"

func main() {
    var f float64 = 1.9
    var i int = int(f)  // 截断小数部分,结果为 1
    
    fmt.Printf("int(1.9): %d\n", i)  // 1
    
    // 四舍五入
    i = int(math.Round(f))
    fmt.Printf("round(1.9): %d\n", i)  // 2
    
    // 溢出检查
    var largeFloat float64 = 1e20
    var smallInt int = int(largeFloat)  // 可能溢出
    fmt.Printf("largeFloat -> int: %d\n", smallInt)
}

2.4.2 类型推断的边界

package main

import "fmt"

func main() {
    // 类型推断
    a := 10           // int
    b := 3.14         // float64
    c := "hello"      // string
    d := true         // bool
    
    // 无法推断(需要显式类型)
    var e  // 编译错误!必须提供类型或初始值
    
    // 混合推断
    f, g, h := 1, 2.5, "test"
    fmt.Printf("f: %T, g: %T, h: %T\n", f, g, h)
}

2.5 运算符体系

2.5.1 算术运算符

a, b := 10, 3

// 加法、减法、乘法、除法、取余
sum := a + b        // 13
diff := a - b       // 7
product := a * b    // 30
quotient := a / b   // 3整数除法
remainder := a % b  // 1

// 注意:整数除法会截断小数部分
fmt.Printf("10 / 3 = %d\n", 10/3)  // 3
fmt.Printf("10.0 / 3.0 = %f\n", 10.0/3.0)  // 3.333333

2.5.2 比较运算符

a, b := 10, 20

// 等于、不等于、大于、小于、大于等于、小于等于
fmt.Println(a == b)  // false
fmt.Println(a != b)  // true
fmt.Println(a > b)   // false
fmt.Println(a < b)   // true
fmt.Println(a >= b)  // false
fmt.Println(a <= b)  // true

注意:字符串也可以比较(按字典序):

fmt.Println("apple" < "banana")  // true

2.5.3 逻辑运算符

a, b := true, false

fmt.Println(a && b)  // false
fmt.Println(a || b)  // true
fmt.Println(!a)      // false

短路求值

func expensive() bool {
    fmt.Println("expensive function called")
    return true
}

if false && expensive() {
    // expensive() 不会被调用
}

2.5.4 位运算符

a, b := 6, 3  // 6 = 110, 3 = 011

fmt.Printf("a & b: %d\n", a & b)    // 2 (010)
fmt.Printf("a | b: %d\n", a | b)    // 7 (111)
fmt.Printf("a ^ b: %d\n", a ^ b)    // 5 (101)
fmt.Printf("a &^ b: %d\n", a &^ b)  // 4 (100) 位清除
fmt.Printf("a << 1: %d\n", a << 1)  // 12 (1100)
fmt.Printf("a >> 1: %d\n", a >> 1)  // 3 (011)

2.5.5 赋值运算符

a := 10

a += 5   // a = a + 5
a -= 3   // a = a - 3
a *= 2   // a = a * 2
a /= 4   // a = a / 4
a %= 3   // a = a % 3

a &= 1   // a = a & 1
a |= 2   // a = a | 2
a ^= 4   // a = a ^ 4
a <<= 1  // a = a << 1
a >>= 1  // a = a >> 1

2.6 流程控制

2.6.1 if-else 语句

score := 85

if score >= 90 {
    fmt.Println("优秀")
} else if score >= 80 {
    fmt.Println("良好")
} else if score >= 60 {
    fmt.Println("及格")
} else {
    fmt.Println("不及格")
}

深度解析

  • if 条件前可以有一个简短语句
  • 简短语句的作用域仅限于 if-else
if x := computeValue(); x > 0 {
    fmt.Println("x 为正数:", x)
} else {
    fmt.Println("x 为非正数:", x)
}
// fmt.Println(x)  // 编译错误x 在 if 块外不可见

2.6.2 switch 语句

day := 3

switch day {
case 1:
    fmt.Println("星期一")
case 2:
    fmt.Println("星期二")
case 3, 4, 5:
    fmt.Println("工作日")
case 6, 7:
    fmt.Println("周末")
default:
    fmt.Println("无效日期")
}

深度解析

  • switch 默认包含 break,不需要手动写
  • 可以使用 fallthrough 继续执行下一个 case
  • 支持无表达式的 switch类似 if-else 链)
// 多值 case
switch day {
case 1, 2, 3, 4, 5:
    fmt.Println("工作日")
case 6, 7:
    fmt.Println("周末")
}

// fallthrough
switch day {
case 1:
    fmt.Println("星期一")
    fallthrough  // 继续执行 case 2
case 2:
    fmt.Println("星期二(包含星期一)")
}

// 无表达式 switch
switch {
case score >= 90:
    fmt.Println("优秀")
case score >= 80:
    fmt.Println("良好")
default:
    fmt.Println("其他")
}

2.6.3 for 循环

Go 只有 for 一种循环结构,但功能强大:

// 1. 传统 for 循环
for i := 0; i < 5; i++ {
    fmt.Println(i)
}

// 2. while 风格(省略初始化)
i := 0
for i < 5 {
    fmt.Println(i)
    i++
}

// 3. 无限循环
for {
    // ...
    break  // 必须退出
}

// 4. range 遍历
slice := []int{1, 2, 3, 4, 5}
for index, value := range slice {
    fmt.Printf("索引 %d: 值 %d\n", index, value)
}

// 5. 只遍历索引
for i := range slice {
    fmt.Println(i)
}

// 6. 只遍历值(使用 _ 忽略索引)
for _, v := range slice {
    fmt.Println(v)
}

深度解析

  • range 遍历字符串时,返回的是 runeUnicode 字符) 而非字节
  • for 循环的初始化语句中的变量,作用域仅限于循环
// 字符串遍历
s := "Hello"
for i, r := range s {
    fmt.Printf("位置 %d: 字符 %c\n", i, r)
}

// 作用域测试
for i := 0; i < 3; i++ {
    fmt.Println(i)
}
// fmt.Println(i)  // 编译错误i 在循环外不可见

2.6.4 break 和 continue

// break跳出当前循环
for i := 0; i < 10; i++ {
    if i == 5 {
        break
    }
    fmt.Println(i)  // 输出 0-4
}

// continue跳过本次循环
for i := 0; i < 10; i++ {
    if i%2 == 0 {
        continue
    }
    fmt.Println(i)  // 输出 1, 3, 5, 7, 9
}

// 带标签的 break/continue跳出多层循环
outer:
for i := 0; i < 3; i++ {
    for j := 0; j < 3; j++ {
        if i == 1 && j == 1 {
            break outer  // 跳出外层循环
        }
        fmt.Printf("i=%d, j=%d\n", i, j)
    }
}

2.7 深度实践:综合案例

2.7.1 简易计算器

package main

import (
    "fmt"
    "strconv"
)

func main() {
    var num1, num2 float64
    var operator string
    
    fmt.Print("请输入第一个数字:")
    fmt.Scan(&num1)
    
    fmt.Print("请输入运算符 (+, -, *, /)")
    fmt.Scan(&operator)
    
    fmt.Print("请输入第二个数字:")
    fmt.Scan(&num2)
    
    var result float64
    var valid bool
    
    switch operator {
    case "+":
        result = num1 + num2
        valid = true
    case "-":
        result = num1 - num2
        valid = true
    case "*":
        result = num1 * num2
        valid = true
    case "/":
        if num2 == 0 {
            fmt.Println("错误:除数不能为零")
            valid = false
        } else {
            result = num1 / num2
            valid = true
        }
    default:
        fmt.Println("错误:无效的运算符")
        valid = false
    }
    
    if valid {
        fmt.Printf("结果:%.2f %s %.2f = %.2f\n", num1, operator, num2, result)
    }
}

2.7.2 素数判断

package main

import "fmt"

func isPrime(n int) bool {
    if n <= 1 {
        return false
    }
    if n == 2 {
        return true
    }
    if n%2 == 0 {
        return false
    }
    
    // 只需检查到 sqrt(n)
    for i := 3; i*i <= n; i += 2 {
        if n%i == 0 {
            return false
        }
    }
    return true
}

func main() {
    fmt.Println("1 到 100 之间的素数:")
    count := 0
    for i := 1; i <= 100; i++ {
        if isPrime(i) {
            fmt.Printf("%d ", i)
            count++
            if count%10 == 0 {
                fmt.Println()
            }
        }
    }
    fmt.Printf("\n共 %d 个素数\n", count)
}

2.8 常见陷阱与最佳实践

2.8.1 变量遮蔽陷阱

func shadowBug() {
    x := 10
    if true {
        x := 20  // 创建了新变量,遮蔽了外层 x
        fmt.Println(x)  // 20
    }
    fmt.Println(x)  // 10外层 x 未变)
    
    // 正确做法:使用 = 赋值
    if true {
        x = 30  // 修改外层 x
        fmt.Println(x)  // 30
    }
    fmt.Println(x)  // 30
}

2.8.2 整数除法陷阱

// 错误:整数除法
result := 5 / 2  // 结果为 2而非 2.5

// 正确:使用浮点数
result := 5.0 / 2.0  // 结果为 2.5
// 或
result := float64(5) / float64(2)

2.8.3 字符串长度陷阱

s := "你好"
fmt.Println(len(s))  // 6字节数不是字符数

// 正确获取字符数
runeCount := len([]rune(s))  // 2
fmt.Println(runeCount)

2.8.4 最佳实践

  1. 优先使用短声明 :=,代码更简洁
  2. 利用零值,减少不必要的初始化
  3. 避免变量遮蔽,保持代码清晰
  4. 显式类型转换,避免精度丢失
  5. 使用 range 遍历,避免手动索引
  6. 注意字符串编码,使用 rune 处理 Unicode

2.9 课后练习

  1. 变量声明:用四种方式声明变量,体会它们的区别
  2. 零值测试:创建各种类型的未初始化变量,观察它们的零值
  3. 类型转换:编写程序测试整数到浮点数的转换,观察精度变化
  4. 字符串遍历:遍历包含中文的字符串,输出每个字符及其 Unicode 码点
  5. 素数统计:统计 1 到 1000 之间的素数个数
  6. 综合练习:编写一个温度转换器(摄氏度 ↔ 华氏度)

2.10 下一步

完成本章后,你将进入第三章:数据结构详解,深入学习数组、切片、映射和结构体,这是 Go 语言最核心的数据组织方式。


代码仓库位置https://giter.top/openclaw/test/tree/main/chapters/chapter-2

下一章预告:数组与切片的区别、映射的底层原理、结构体嵌入与组合