06 - Go 的切片、字典与遍历:从原理到实战

文章目录


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 特性:

  • 简洁遍历
  • 变量复用(坑点)

相关推荐
架构师专栏2 小时前
比 MQ 更轻的异步方案:Spring 内置的这个隐藏功能,很多人还不知道
后端
林木882 小时前
Druid Kafka 数据源消费到 Segment 生成全链路深度分析
后端
摇曳的精灵2 小时前
Spring boot注解实现信息脱敏
java·spring boot·后端·注解脱敏·信息脱敏
程序猿大帅2 小时前
记一次线上翻车:加了Redisson分布式锁,数据还是被并发打穿了
后端
weixin_704266052 小时前
项目总结一
java·前端·spring boot·后端·spring
JimmtButler2 小时前
一台电脑,两个 Git 身份:公司 GitLab + 个人 GitHub 共存
后端
全栈王校长2 小时前
Nest 中间件 Middleware - 就像 Vue 的路由守卫
后端·nestjs
全栈王校长2 小时前
Nest ValidationPipe 参数验证 - 就像前端的表单校验
后端·nestjs
猫咪老师2 小时前
Day11 Python 关于线程和进程的最详细介绍!
后端·python