Skip to content

第05章 集合类型 —— 数组、切片、map、字符串

本章目标:搞清楚 Go 中最容易踩坑的部分:切片底层共享、map 的零值规则、字符串与 rune 的关系。


1. 数组(Array):固定长度

1.1 声明

go
var a [5]int                      // [0 0 0 0 0]
b := [3]string{"Go", "Rust", "C"}
c := [...]int{1, 2, 3, 4}         // 编译器自动推断长度 = 4

1.2 数组特点

  • 长度是类型的一部分[3]int[5]int不同类型,不能赋值
  • 数组是值类型:拷贝整个数组,函数传参也是拷贝

🌰 生活化比喻:数组像固定 5 格的药盒,每天吃一格,盒子格数定死了,不能拆。

go
func mod(arr [3]int) { arr[0] = 100 }
a := [3]int{1, 2, 3}
mod(a)
fmt.Println(a)   // [1 2 3] 不变!

📊 配套图解:diagrams/array-vs-slice.svg

💡 实际开发中几乎不用数组,都用切片。


2. 切片(Slice):Go 的灵魂

2.1 切片是什么?

切片是对底层数组的视图,由三部分组成:

┌─────────────┐
│ ptr  → [底层数组]
│ len  = 当前长度
│ cap  = 底层数组容量
└─────────────┘

🌰 生活化比喻:切片像电视机的画面缩放——底层数组是整个电视屏幕,切片是你"圈定"的一块区域,可以放大缩小,但实际内容还在屏幕上。

2.2 创建切片的几种方式

go
s1 := []int{1, 2, 3}              // 字面量
s2 := make([]int, 5)              // 长度 5,容量 5
s3 := make([]int, 5, 10)          // 长度 5,容量 10
s4 := []int(nil)                  // nil 切片
s5 := []int{}                     // 空切片(len=0, cap=0,但非 nil)

2.3 append 的扩容机制

go
s := []int{1, 2, 3}
s = append(s, 4)
s = append(s, 5, 6, 7)

扩容规则(Go 1.18+)

  • 容量 < 256:翻倍
  • 容量 ≥ 256:每次 +25%(缓增)

📊 配套图解:diagrams/slice-internals.svg

2.4 ⚠️ 切片的"共享"陷阱

go
a := []int{1, 2, 3, 4, 5}
b := a[1:3]    // [2, 3]
b[0] = 99
fmt.Println(a)   // [1 99 3 4 5]   a 也被改了!

原因ba 共享同一个底层数组。

如果想真正复制,用 copyappend

go
b := append([]int(nil), a[1:3]...)   // 真复制
// 或
b := make([]int, 2)
copy(b, a[1:3])

📊 配套图解:diagrams/slice-share.svg

2.5 切片操作速查

操作写法
添加s = append(s, x)
删除中间s = append(s[:i], s[i+1:]...)
删除末尾s = s[:len(s)-1]
复制dst := make([]int, len(src)); copy(dst, src)
反转slices.Reverse(s) (Go 1.21+)
排序sort.Ints(s)slices.Sort(s)

3. Map:键值对字典

3.1 创建与使用

go
m := make(map[string]int)
m["Alice"] = 90
m["Bob"] = 85

// 字面量
scores := map[string]int{
    "Alice": 90,
    "Bob":   85,
}

3.2 关键操作

go
// 读取(重要:双返回值检测 key 是否存在)
v, ok := m["Alice"]
if ok { fmt.Println("找到", v) }

// 删除
delete(m, "Alice")

// 长度
len(m)

// 遍历(顺序随机!)
for k, v := range m {
    fmt.Println(k, v)
}

3.3 ⚠️ map 的坑

坑1:nil map 不能写

go
var m map[string]int   // nil map
m["a"] = 1             // ❌ panic!

m = make(map[string]int)
m["a"] = 1             // ✅

🌰 生活化比喻:你拿到一个空地址的钥匙——根本没有房子,怎么往里塞东西?

坑2:map 是"引用"类型

go
m1 := map[string]int{"a": 1}
m2 := m1
m2["b"] = 2
fmt.Println(m1)   // map[a:1 b:2]   m1 也被改了

坑3:遍历顺序随机

go
for k, v := range m {
    // 顺序每次都不同!这是 Go 故意的,防止你依赖顺序
}

要有序遍历,先把 key 排序:

go
keys := make([]string, 0, len(m))
for k := range m {
    keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
    fmt.Println(k, m[k])
}

坑4:并发不安全

map 不是并发安全的。多 goroutine 读写要用 sync.RWMutexsync.Map(第09章细讲)。

📊 配套图解:diagrams/map-internals.svg


4. 字符串:高级版字节切片

4.1 字符串本质

go
s := "Hello, 世界"
// 底层:{ data *byte; len int }
// data 指向只读内存

4.2 字符串常用操作

go
import "strings"

strings.ToUpper("hello")           // "HELLO"
strings.Contains("hello", "ll")    // true
strings.Split("a,b,c", ",")        // ["a", "b", "c"]
strings.Join([]string{"a","b"}, "-") // "a-b"
strings.Replace("hello", "l", "L", 1) // "heLlo"
strings.TrimSpace("  hi  ")        // "hi"

4.3 字符串与 byte/rune 互转

go
s := "Hi你好"

// string -> []byte
b := []byte(s)

// string -> []rune(按字符)
r := []rune(s)
fmt.Println(len(r))   // 4

// []byte -> string
s2 := string(b)

4.4 高效拼接:strings.Builder

go
var b strings.Builder
for i := 0; i < 1000; i++ {
    b.WriteString("hello")
}
result := b.String()

⚠️ 不要用 + 拼接大量字符串,每次都会分配新内存。

📊 配套图解:diagrams/string-bytes-runes.svg


5. make vs new

make(T, ...)new(T)
适用类型slice, map, chan任何类型
返回类型 T 本身*T(指针)
初始化完整初始化仅分配内存(零值)
go
s := make([]int, 5)       // []int 类型,可立即用
m := make(map[string]int) // 可立即用
p := new(int)             // *int,*p == 0

6. 综合案例:词频统计

go
package main

import (
    "fmt"
    "sort"
    "strings"
)

func main() {
    text := "go is fun go is simple go runs fast"
    words := strings.Fields(text)
    
    freq := make(map[string]int)
    for _, w := range words {
        freq[w]++
    }
    
    type kv struct { key string; val int }
    var sorted []kv
    for k, v := range freq {
        sorted = append(sorted, kv{k, v})
    }
    sort.Slice(sorted, func(i, j int) bool { return sorted[i].val > sorted[j].val })
    
    for _, kv := range sorted {
        fmt.Printf("%s: %d\n", kv.key, kv.val)
    }
}

输出:

go: 3
is: 2
fun: 1
simple: 1
runs: 1
fast: 1

7. 本章小结

✅ 数组长度是类型一部分,是值类型——实战中很少用
✅ 切片是指针 + len + cap 的视图,是 Go 最常用的容器
✅ 切片可能因共享底层数组而出现"诡异"修改
✅ map 必须 make 初始化才能写
✅ map 遍历顺序故意随机
✅ 字符串底层是字节序列,索引按字节,遍历 rune 才是按字符
✅ 大量字符串拼接用 strings.Builder


📂 配套资源