数组-固定长度的集合

  1. 长度是类型的一部分:[5]int 是一个与 [3]int 完全不同的类型。你不能将一个长度为5的数组赋值给一个长度为3的数组变量。

  2. 值类型:当你在函数间传递数组时,Go会复制整个数组。这与Python的list(传递的是引用)有本质区别。这意味着在函数内修改数组不会影响函数外的原始数组。

package main

import "fmt"

func main() {
    // 1. 声明并初始化一个数组
    var a [4]int // 声明一个包含4个整数的数组,元素自动初始化为0
    fmt.Println(a) // 输出: [0 0 0 0]

    // 2. 使用字面量初始化
    b := [4]int{1, 2, 3, 4}
    fmt.Println(b) // 输出: [1 2 3 4]

    // 3. 让编译器计算长度
    c := [...]string{"Go", "Python", "Java"}
    fmt.Println(c) // 输出: [Go Python Java]
    fmt.Printf("c的类型是: %T, 长度是: %d\n", c, len(c)) // 输出: c的类型是: [3]string, 长度是: 3

    // 4. 访问和修改元素
    b[0] = 10
    fmt.Println(b) // 输出: [10 2 3 4]

    // 5. 数组是值类型(演示)
    modifyArray(a)
    fmt.Println("main函数中的a:", a) // 输出: main函数中的a: [0 0 0 0] (未被改变)
}

func modifyArray(arr [4]int) {
    arr[0] = 99
    fmt.Println("modifyArray函数中的arr:", arr) // 输出: modifyArray函数中的arr: [99 0 0 0]
}

数组在Go中不常用,因为其固定长度和复制成本太高。它更多是作为切片的底层基础而存在。理解数组的工作原理,是理解切片的关键。

切片-动态、灵活的视图

切片是Go中最常用的数据结构。它的设计非常巧妙,既提供了动态数组的便利性,又通过共享底层数组实现了高效的内存操作。记住,append 可能会返回一个新的切片,所以总是要用 s = append(s, ...) 的形式。

核心特点:

  1. 引用类型:切片本身只是一个包含了三个信息的数据结构:指向底层数组的指针、长度 和 容量。当传递切片时,实际上是在复制这个小小的“头部信息”,所以多个切片可以共享同一个底层数组,修改一个会影响其他。

  2. 动态长度:你可以使用 append 函数轻松地为切片添加元素,它会自动处理底层数组的扩容。

  3. 长度 vs. 容量:这是理解切片的关键!

  • 长度:切片当前包含的元素个数。用 len(s) 获取。

  • 容量:从切片的起始元素到底层数组的最后一个元素的总个数。用 cap(s) 获取。append 操作在容量不足时会触发扩容,创建一个新的、更大的底层数组。

package main

import "fmt"

func main() {
    // 1. 从数组创建切片
    arr := [5]int{10, 20, 30, 40, 50}
    var s []int = arr[1:4] // 创建一个从arr[1]到arr[3]的切片
    fmt.Println(s) // 输出: [20 30 40]
    fmt.Printf("len=%d, cap=%d\n", len(s), cap(s)) // 输出: len=3, cap=4 (容量是到arr末尾)

    // 2. 直接声明和初始化切片
    s1 := []int{1, 2, 3} // 注意没有 [...]
    fmt.Println(s1) // 输出: [1 2 3]

    // 3. 使用 make 创建切片 (指定长度和容量)
    // make([]T, length, capacity)
    s2 := make([]int, 3, 5) // 创建一个长度为3,容量为5的int切片,元素初始化为0
    fmt.Println(s2) // 输出: [0 0 0]
    fmt.Printf("len=%d, cap=%d\n", len(s2), cap(s2)) // 输出: len=3, cap=5

    // 4. append 操作
    s2 = append(s2, 100) // 添加一个元素
    fmt.Println(s2) // 输出: [0 0 0 100]
    fmt.Printf("len=%d, cap=%d\n", len(s2), cap(s2)) // 输出: len=4, cap=5

    s2 = append(s2, 200, 300) // 添加多个元素,容量会不足
    fmt.Println(s2) // 输出: [0 0 0 100 200 300]
    fmt.Printf("len=%d, cap=%d\n", len(s2), cap(s2)) // 输出: len=6, cap=10 (容量自动翻倍扩容)
}

Map - 键值对的无序集合

Map是Go中进行查找和关联操作的首选数据结构。它的使用非常直观,但要时刻记住访问不存在的键会返回零值,所以在需要区分“零值”和“不存在”时,一定要用 value, ok := myMap[key] 这种地道的写法。

核心特点:

  1. 引用类型:和切片一样,传递的是引用。

  2. 无序:Map中的元素没有固定的顺序。(注意:在Go 1.0之后,每次运行程序,map的迭代顺序都是随机的,这是为了防止程序依赖特定的顺序)。

  3. 键的类型:map的键必须是可比较的类型(例如,数字、字符串、指针、数组),不能是切片、map或函数。

  4. 安全的访问方式:访问一个不存在的键时,会返回该值类型的“零值”(例如,int返回0,string返回"")。Go提供了一个特殊的“逗号ok”语法来判断一个键是否真正存在

package main

import "fmt"

func main() {
    // 1. 声明和初始化
    var m map[string]int // 声明一个map,此时是nil
    fmt.Println(m == nil) // 输出: true

    // 使用 make 创建一个可用的map
    m = make(map[string]int)
    m["apple"] = 5
    m["banana"] = 3
    fmt.Println(m) // 输出: map[apple:5 banana:3]

    // 2. 使用字面量初始化
    ageMap := map[string]int{
        "Alice": 25,
        "Bob":   30,
    }
    fmt.Println(ageMap)

    // 3. 访问元素
    fmt.Println("Alice's age:", ageMap["Alice"]) // 输出: Alice's age: 25

    // 4. “逗号ok”判断键是否存在
    // 如果键不存在,value会得到int的零值0,ok为false
    value, ok := ageMap["Charlie"]
    if ok {
        fmt.Println("Charlie's age is", value)
    } else {
        fmt.Println("Charlie not found") // 输出: Charlie not found
    }

    // 5. 删除元素
    delete(ageMap, "Bob")
    fmt.Println(ageMap) // 输出: map[Alice:25]
}