文章目录
- [06 - Go 的切片、字典与遍历:从原理到实战(建议收藏🔥)](#06 - Go 的切片、字典与遍历:从原理到实战(建议收藏🔥))
- 切片(Slice)详解
-
- 什么是切片?
- 切片的创建方式
- [len 和 cap 区别](#len 和 cap 区别)
- [append 扩容机制(重点🔥)](#append 扩容机制(重点🔥))
- 切片是引用类型(坑点)
- 拷贝切片(避免数据污染)
- 字典(Map)详解
- 遍历(range)详解
-
- 遍历切片
- [遍历 map](#遍历 map)
- [遍历字符串(支持 Unicode🔥)](#遍历字符串(支持 Unicode🔥))
- [range 常见坑(必须掌握)](#range 常见坑(必须掌握))
-
- [range 变量复用问题](#range 变量复用问题)
- 修改切片长度问题
- [map 遍历时修改](#map 遍历时修改)
- 实战案例
- 总结(面试重点🔥)
06 - Go 的切片、字典与遍历:从原理到实战(建议收藏🔥)
在 Go 语言中,切片(slice)和字典(map) 是最常用的数据结构,而遍历(range) 则是操作它们的核心手段。
很多初学者"会用但不懂",比如:
- 为什么切片 append 会扩容?
- map 为什么是无序的?
- range 遍历为什么会踩坑?
这篇文章带你一次讲透。
切片(Slice)详解
什么是切片?
切片是对数组的动态视图(引用类型):
go
package main
import "fmt"
func main() {
arr := [5]int{1, 2, 3, 4, 5}
s := arr[2:4]
fmt.Println(arr)
fmt.Println(s)
fmt.Println(len(arr))
fmt.Println(len(s))
}
输出:
bath
[1 2 3 4 5]
[3 4]
5
2
切片本质结构(底层):
go
type slice struct {
ptr *T // 指向底层数组
len int // 当前长度
cap int // 容量
}
切片的创建方式
基于数组创建
go
package main
import "fmt"
func main() {
arr := [3]int{1, 2, 3}
s := arr[:]
fmt.Println(arr)
fmt.Println(s)
fmt.Println(len(arr))
fmt.Println(len(s))
}
输出:
bath
[1 2 3]
[1 2 3]
3
3
使用 make创建(最常用)
👉 优点:因为限制了容量
- 避免多次扩容
- 减少 copy
- 性能更高
go
package main
import "fmt"
func main() {
arr := make([]int, 3, 5)
fmt.Println(arr)
fmt.Println(len(arr))
fmt.Println(cap(arr))
}
//arr := make([]int, 3, 5) // len=3, cap=5
输出:
bath
[0 0 0]
3
5
直接初始化创建
go
package main
import "fmt"
func main() {
arr := []int{1, 2, 3}
fmt.Println(arr)
fmt.Println(len(arr))
fmt.Println(cap(arr))
}
输出:
bath
[1 2 3]
3
3
len 和 cap 区别
go
s := make([]int, 3, 5)
fmt.Println(len(s)) // 3
fmt.Println(cap(s)) // 5
- len:当前可用元素个数
- cap:底层数组容量
append 扩容机制(重点🔥)
go
package main
import "fmt"
func main() {
arr := []int{1, 2, 3}
arr = append(arr, 5)
fmt.Println(arr)
fmt.Println(len(arr))
fmt.Println(cap(arr))
}
输出:
bath
[1 2 3 5]
4
6
扩容规则(简化理解):
- 小于 1024:翻倍
- 大于 1024:按 1.25 倍增长
⚠️ 扩容后:
- 可能会创建新数组
- 原切片和新切片不再共享数据
切片是引用类型(坑点)
go
package main
import "fmt"
func main() {
arr := []int{1, 2, 3}
arr2 := arr
arr2[0] = 100
fmt.Println(arr)
fmt.Println(len(arr))
fmt.Println(cap(arr))
fmt.Println("======")
fmt.Println(arr2)
fmt.Println(len(arr2))
fmt.Println(cap(arr2))
}
输出:
bath
[100 2 3]
3
3
======
[100 2 3]
3
3
👉 因为指向同一个底层数组
拷贝切片(避免数据污染)
go
package main
import "fmt"
func main() {
arr := []int{1, 2, 3}
arr2 := make([]int, len(arr))
copy(arr2, arr)
fmt.Println(arr)
fmt.Println(len(arr))
fmt.Println(cap(arr))
fmt.Println("======")
fmt.Println(arr2)
fmt.Println(len(arr2))
fmt.Println(cap(arr2))
}
输出:
bath
[1 2 3]
3
3
======
[1 2 3]
3
3
字典(Map)详解
什么是 map?
Go 的 map 是:
👉 键值对集合(无序)
go
package main
import "fmt"
func main() {
m := map[string]int{
"one": 1,
"two": 2,
"three": 3,
"four": 4,
}
fmt.Println(m)
}
输出:
bath
map[four:4 one:1 three:3 two:2]
创建 map
字面量
go
package main
import "fmt"
func main() {
m := map[string]int{"a": 1}
fmt.Println(m)
}
输出:
bath
map[a:1]
make
go
package main
import "fmt"
func main() {
m := make(map[string]int)
m["b"] = 2
fmt.Println(m)
}
输出:
bath
map[b:2]
基本操作
增 / 改
go
m["a"] = 100
查
go
v := m["a"]
判断是否存在(重要🔥)
go
v, ok := m["b"]
if ok {
fmt.Println("存在", v)
}
删除
go
delete(m, "a")
示例
go
package main
import "fmt"
func main() {
m := map[string]int{
"one": 1,
"two": 2,
"three": 3,
"four": 4,
}
fmt.Println("原", m)
m["five"] = 5
fmt.Println("增", m)
delete(m, "two")
fmt.Println("删", m)
m["one"] = 10
fmt.Println("改", m)
fmt.Println("查", m["three"])
// 判断
if v, ok := m["six"]; ok {
fmt.Println("six", v)
} else {
fmt.Println("six 不存在")
}
if v, ok := m["three"]; ok {
fmt.Println("three", v)
} else {
fmt.Println("three 不存在")
}
}
输出:
bath
原 map[four:4 one:1 three:3 two:2]
增 map[five:5 four:4 one:1 three:3 two:2]
删 map[five:5 four:4 one:1 three:3]
改 map[five:5 four:4 one:10 three:3]
查 3
six 不存在
three 3
map 是无序的(核心特性)
go
for k, v := range m {
fmt.Println(k, v)
}
👉 每次输出顺序都可能不同!
原因:
- Go 为防止依赖顺序
- 内部做了随机化
map 是引用类型
go
package main
import "fmt"
func main() {
m1 := map[string]int{"a": 1}
m2 := m1
m2["a"] = 999
fmt.Println(m1["a"]) // 999
}
输出:
bath
999
遍历(range)详解
遍历切片
go
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
for k, v := range s {
fmt.Println(k, v)
}
}
输出:
bath
0 1
1 2
2 3
只要值:
go
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
for _, v := range s {
fmt.Println(v)
}
}
输出:
bath
1
2
3
遍历 map
go
package main
import "fmt"
func main() {
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
fmt.Println(k, v)
}
}
输出:
bath
a 1
b 2
遍历字符串(支持 Unicode🔥)
go
package main
import "fmt"
func main() {
s := "你好"
for k, v := range s {
fmt.Println(k, v)
}
for k, v := range s {
fmt.Println(k, string(v))
}
}
输出:
bath
0 20320
3 22909
0 你
3 好
👉 v 是 rune(Unicode 编码)
range 常见坑(必须掌握)
range 变量复用问题
⚠️ 注意:range 变量复用问题在 Go 1.22 已被修复
在 Go 1.21 及之前版本中:
range 循环中的变量是复用的,会导致取地址或闭包引用时出现问题
在 Go 1.22 及之后版本中:
每次循环都会创建新的变量,不再存在该问题
因此:
- 新项目(Go ≥ 1.22):无需额外处理
- 老项目:仍建议使用 vCopy 方式避免风险
go
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
var ptrs []*int
for _, v := range s {
vCopy := v
ptrs = append(ptrs, &vCopy)
fmt.Println(v)
fmt.Println(&vCopy)
}
fmt.Println(ptrs)
fmt.Println(*ptrs[0])
fmt.Println(*ptrs[1])
fmt.Println(*ptrs[2])
}
输出:
bath
1
0xc000010120
2
0xc000096010
3
0xc000096018
[0xc000010120 0xc000096010 0xc000096018]
1
2
3
修改切片长度问题
go
for i, v := range s {
s = append(s, v) // ❌ 不建议
}
👉 会导致不可预期行为
map 遍历时修改
go
for k := range m {
delete(m, k) // ✅ 安全(Go 特性)
}
👉 Go 允许在遍历时删除
实战案例
去重切片
go
package main
import "fmt"
func main() {
fmt.Println(unique([]int{1, 2, 3, 2, 4, 1}))
}
func unique(nums []int) []int {
m := make(map[int]struct{})
var res []int
for _, v := range nums {
if _, ok := m[v]; !ok {
m[v] = struct{}{}
res = append(res, v)
}
}
return res
}
输出:
bath
[1 2 3 4]
统计词频
go
package main
import "fmt"
func main() {
fmt.Println(wordCount([]string{"hello", "world", "hello"}))
}
func wordCount(words []string) map[string]int {
m := make(map[string]int)
for _, w := range words {
m[w]++
}
return m
}
输出:
bath
map[hello:2 world:1]
总结(面试重点🔥)
✅ 切片本质:
- 引用类型(指向数组)
- 有 len 和 cap
✅ map 特点:
- 无序
- 引用类型
- key 唯一
✅ range 特性:
- 简洁遍历
- 变量复用(坑点)