目录
- [🟢 Go 入门到精通 - 复合类型之数组与切片](#🟢 Go 入门到精通 - 复合类型之数组与切片)
-
- 一、引言:Go复合类型的基石
- 二、数组:固定长度的序列
-
- [2.1 数组的声明与初始化](#2.1 数组的声明与初始化)
- [2.2 长度是类型的一部分](#2.2 长度是类型的一部分)
- [2.3 数组的遍历](#2.3 数组的遍历)
- [2.4 数组作为函数参数](#2.4 数组作为函数参数)
- 三、切片:动态数组的完美实现
-
- [3.1 切片是什么](#3.1 切片是什么)
- [3.2 切片的底层结构](#3.2 切片的底层结构)
- [3.3 创建切片的多种方式](#3.3 创建切片的多种方式)
- [3.4 make函数详解](#3.4 make函数详解)
- 四、切片的核心操作
-
- [4.1 切片截取](#4.1 切片截取)
- [4.2 append操作](#4.2 append操作)
- [4.3 copy操作](#4.3 copy操作)
- 五、切片扩容机制深度解析
-
- [5.1 Go 1.18前后的扩容策略](#5.1 Go 1.18前后的扩容策略)
- [5.2 扩容示例演示](#5.2 扩容示例演示)
- 六、切片作为函数参数
- [七、数组 vs 切片完整对比](#七、数组 vs 切片完整对比)
- 八、常见陷阱与避坑指南
- 九、实战案例
- 十、小结与预告
-
- [📝 核心知识点回顾](#📝 核心知识点回顾)
- [🤔 互动问题](#🤔 互动问题)
- [📖 下篇预告](#📖 下篇预告)
- [📚 参考资料](#📚 参考资料)
🟢 Go 入门到精通 - 复合类型之数组与切片
📅 更新于 2026年7月 | ✍️ 原创文章,转载请注明出处 | 🧑💻 作者:布朗克168
一、引言:Go复合类型的基石
如果说基本类型是Go语言的"原子",那么复合类型就是"分子"。在Go的复合类型体系中有四大天王:
┌──────────────────────────────────────────────────────────────┐
│ Go 复合类型四大天王 │
├──────────────┬──────────────┬──────────────┬─────────────────┤
│ 数组 │ 切片 │ Map │ Struct │
│ [N]T │ []T │ map[K]V │ struct{} │
├──────────────┼──────────────┼──────────────┼─────────────────┤
│ 固定长度 │ 动态长度 │ 键值对 │ 字段集合 │
│ 值类型 │ 引用语义 │ 引用类型 │ 值类型 │
│ 类型的一部分 │ 最常用! │ 无序 │ 自定义类型 │
└──────────────┴──────────────┴──────────────┴─────────────────┘
数组和切片是这四大天王中关系最紧密的两个------切片建立在数组之上,理解了它们的关系,才算真正入门了Go。
🎯 核心认知:数组是切片的"地基",切片是数组的"智能窗口"。90%的Go代码使用切片而非数组,但理解数组才能理解切片的底层行为。
二、数组:固定长度的序列
2.1 数组的声明与初始化
在Go中,数组是固定长度、同类型元素的序列。长度一旦确定,不可改变。
go
package main
import "fmt"
func main() {
// === 声明方式 ===
// 1. 声明但不初始化(元素为零值)
var arr1 [5]int
fmt.Println(arr1) // [0 0 0 0 0]
// 2. 声明并初始化
var arr2 [5]int = [5]int{1, 2, 3, 4, 5}
fmt.Println(arr2) // [1 2 3 4 5]
// 3. 短声明 + 初始化
arr3 := [5]int{10, 20, 30, 40, 50}
fmt.Println(arr3) // [10 20 30 40 50]
// 4. 让编译器推断长度
arr4 := [...]int{1, 2, 3, 4, 5, 6} // 长度自动为6
fmt.Printf("arr4 长度: %d, 值: %v\n", len(arr4), arr4)
// 输出:arr4 长度: 6, 值: [1 2 3 4 5 6]
// 5. 指定索引初始化
arr5 := [10]int{0: 1, 4: 5, 9: 10}
fmt.Println(arr5)
// 输出:[1 0 0 0 5 0 0 0 0 10]
// 6. 多维数组
matrix := [3][4]int{
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
}
fmt.Println(matrix[1][2]) // 7
}
2.2 长度是类型的一部分
这是Go数组最核心也最容易被忽视的特性:
go
var a [3]int
var b [5]int
// a 的类型是 [3]int
// b 的类型是 [5]int
// [3]int 和 [5]int 是两种不同的类型!
// a = b // ❌ 编译错误:cannot use b (type [5]int) as type [3]int
这意味着:
| 特性 | 影响 |
|---|---|
| 函数参数 | func f(arr [5]int) 只能接收长度为5的数组 |
| 类型断言 | 不能将[3]int断言为[5]int |
| 接口匹配 | [3]int和[5]int满足不同的接口约束 |
| 实用后果 | 这导致数组作为函数参数几乎不可用------因为长度写死了 |
这就是为什么Go中切片远比数组常用。
2.3 数组的遍历
go
arr := [5]string{"🍎", "🍌", "🍊", "🍇", "🍓"}
// 方式1:传统for
for i := 0; i < len(arr); i++ {
fmt.Printf("arr[%d] = %s\n", i, arr[i])
}
// 方式2:range迭代
for i, fruit := range arr {
fmt.Printf("第%d个水果: %s\n", i+1, fruit)
}
// 方式3:只关心值
for _, fruit := range arr {
fmt.Print(fruit, " ")
}
2.4 数组作为函数参数
go
package main
import "fmt"
// 数组是值类型------传递时会发生完整拷贝!
func modifyArray(arr [5]int) {
arr[0] = 999 // 只修改了副本
fmt.Println("函数内:", arr) // [999 2 3 4 5]
}
func main() {
original := [5]int{1, 2, 3, 4, 5}
modifyArray(original)
fmt.Println("函数外:", original) // [1 2 3 4 5] 不受影响!
// 如果数组很大(如[1000000]int),复制开销巨大
// 解决方法:传递数组指针
}
// ✅ 传递指针可以避免拷贝
func modifyArrayPtr(arr *[5]int) {
arr[0] = 999 // 通过指针修改原数组
}
⚠️ 值传递的开销 :一个
[1000000]int数组大小约8MB,每次传参都要完整复制。这就是为什么Go中几乎不直接使用大数组。
三、切片:动态数组的完美实现
3.1 切片是什么
切片(slice)是Go中最常用的数据结构。它是一个对底层数组的引用,提供动态大小、灵活的子序列视图。
go
// 切片声明:没有指定长度
var s []int // 这就是切片!nil切片,len=0, cap=0
// 对比:
var arr [5]int // 数组,指定了长度
3.2 切片的底层结构
每个切片在运行时由三个字段组成:
┌──────────────────────────────────────────────────┐
│ 切片结构体 (reflect.SliceHeader) │
├──────────┬──────────┬─────────────────────────────┤
│ ptr │ len │ cap │
│ *T │ int │ int │
├──────────┼──────────┼─────────────────────────────┤
│ 指向底层 │ 当前长度 │ 容量(从ptr开始到底层数组末尾) │
│ 数组元素 │ │ │
└──────────┴──────────┴─────────────────────────────┘
图示理解:
底层数组: [A] [B] [C] [D] [E] [F] [G] [H]
↑ ↑
ptr 底层数组末尾
|-- len=3 --|
|----------- cap=7 -----------|
切片 s := arr[0:3]
s[0]=A, s[1]=B, s[2]=C
剩余容量 4 个位置可用于append
go
package main
import "fmt"
func main() {
arr := [8]string{"A", "B", "C", "D", "E", "F", "G", "H"}
// 创建切片:arr[0:3]
s := arr[0:3]
fmt.Printf("len=%d, cap=%d, %v\n", len(s), cap(s), s)
// 输出:len=3, cap=8, [A B C]
// 访问切片元素
fmt.Println(s[0]) // A
fmt.Println(s[2]) // C
// 切片共享底层数组
s[0] = "X"
fmt.Println("arr:", arr) // [X B C D E F G H]
fmt.Println("s: ", s) // [X B C]
// arr也被修改了!
}
3.3 创建切片的多种方式
go
package main
import "fmt"
func main() {
// === 方式1:字面量(底层自动创建数组) ===
s1 := []int{1, 2, 3, 4, 5}
fmt.Printf("s1: len=%d cap=%d %v\n", len(s1), cap(s1), s1)
// s1: len=5 cap=5 [1 2 3 4 5]
// === 方式2:从数组切片 ===
arr := [8]int{10, 20, 30, 40, 50, 60, 70, 80}
s2 := arr[1:5] // 索引1到4(左闭右开)
fmt.Printf("s2: len=%d cap=%d %v\n", len(s2), cap(s2), s2)
// s2: len=4 cap=7 [20 30 40 50]
// === 方式3:从切片切片 ===
s3 := s2[1:3] // s2是切片,s3也是切片
fmt.Printf("s3: len=%d cap=%d %v\n", len(s3), cap(s3), s3)
// s3: len=2 cap=6 [30 40]
// === 方式4:make函数 ===
s4 := make([]int, 5) // len=5, cap=5
s5 := make([]int, 5, 10) // len=5, cap=10
fmt.Printf("s4: len=%d cap=%d\n", len(s4), cap(s4))
fmt.Printf("s5: len=%d cap=%d %v\n", len(s5), cap(s5), s5)
// s5: len=5 cap=10 [0 0 0 0 0]
// === 方式5:nil切片 vs 空切片 ===
var nilSlice []int // nil切片,len=0, cap=0, ptr=nil
emptySlice := []int{} // 空切片,len=0, cap=0, ptr有值
makeEmpty := make([]int, 0) // 空切片,len=0, cap=0
fmt.Println(nilSlice == nil) // true
fmt.Println(emptySlice == nil) // false
}
3.4 make函数详解
make是Go中专门用于创建切片、map、channel的内置函数:
go
make([]T, len) // 创建len=cap的切片
make([]T, len, cap) // 创建指定len和cap的切片
go
package main
import "fmt"
func main() {
// 场景1:知道大概需要多少元素------预分配容量
users := make([]string, 0, 1000) // 预分配1000容量,避免多次扩容
for i := 0; i < 1000; i++ {
users = append(users, fmt.Sprintf("user_%d", i))
}
fmt.Printf("最终: len=%d cap=%d\n", len(users), cap(users))
// 输出:最终: len=1000 cap=1000(可能更大,取决于扩容策略)
// 场景2:需要指定初始值------带长度的make
scores := make([]int, 10) // 前10个位置都是零值0
for i := 0; i < 10; i++ {
scores[i] = i * 10 // 直接赋值,不需要append
}
fmt.Println(scores) // [0 10 20 30 40 50 60 70 80 90]
}
四、切片的核心操作
4.1 切片截取
切片的截取语法[low:high:max]是最灵活的特性之一:
go
package main
import "fmt"
func main() {
arr := [10]int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// 基本截取 [low:high] ------ 左闭右开
s1 := arr[2:5] // 索引 2,3,4
fmt.Printf("arr[2:5]: len=%d cap=%d %v\n", len(s1), cap(s1), s1)
// arr[2:5]: len=3 cap=8 [2 3 4]
// 省略low:从0开始
s2 := arr[:5] // arr[0:5]
fmt.Println("arr[:5]:", s2) // [0 1 2 3 4]
// 省略high:到末尾
s3 := arr[5:] // arr[5:10]
fmt.Println("arr[5:]:", s3) // [5 6 7 8 9]
// 省略两者:复制整个数组为切片
s4 := arr[:]
fmt.Println("arr[:]:", s4) // [0 1 2 3 4 5 6 7 8 9]
// 三索引切片 [low:high:max] ------ 控制容量
// 限制cap,防止子切片访问超出预期的底层数组
s5 := arr[2:5:6] // len=3, cap=4 (max-low=6-2)
fmt.Printf("arr[2:5:6]: len=%d cap=%d %v\n", len(s5), cap(s5), s5)
// arr[2:5:6]: len=3 cap=4 [2 3 4]
// ❌ max不能超过底层数组长度,high不能超过max
// s6 := arr[2:5:4] // 编译错误:invalid slice index: 5 > 4
}
截取规则速查表:
| 表达式 | len | cap | 含义 |
|---|---|---|---|
s[:] |
len(s) | cap(s) | 完整切片 |
s[:n] |
n | cap(s) | 前n个 |
s[n:] |
len(s)-n | cap(s)-n | 从第n个到末尾 |
s[m:n] |
n-m | cap(s)-m | 从m到n-1 |
s[m:n:k] |
n-m | k-m | 限制容量为k-m |
4.2 append操作
append是切片最常用的操作,用于向切片末尾追加元素:
go
package main
import "fmt"
func main() {
// 基本append
s := []int{1, 2, 3}
s = append(s, 4)
fmt.Println(s) // [1 2 3 4]
// 追加多个元素
s = append(s, 5, 6, 7)
fmt.Println(s) // [1 2 3 4 5 6 7]
// 追加另一个切片(需要...展开)
s2 := []int{8, 9, 10}
s = append(s, s2...)
fmt.Println(s) // [1 2 3 4 5 6 7 8 9 10]
// ⚠️ 注意:append的返回值必须接收!
// append可能触发扩容,返回新的切片头
// 如果不接收返回值,追加可能丢失
// ❌ 错误做法
_ = append(s, 100)
fmt.Println(s) // [1 2 3 4 5 6 7 8 9 10] ------ 没有100!
// ✅ 正确做法
s = append(s, 100)
fmt.Println(s) // [1 2 3 4 5 6 7 8 9 10 100]
}
append的核心规则:
如果 len + 新元素数 <= cap:
→ 直接在底层数组上追加,返回原切片头(但len增加)
如果 len + 新元素数 > cap:
→ 分配新的底层数组,复制原数据,追加新元素,返回新切片头
4.3 copy操作
copy用于将一个切片的数据复制到另一个切片:
go
package main
import "fmt"
func main() {
src := []int{1, 2, 3, 4, 5}
dst := make([]int, 3) // len=3
// copy返回实际复制的元素个数(取min(len(src), len(dst)))
n := copy(dst, src)
fmt.Printf("复制了%d个元素: %v\n", n, dst)
// 复制了3个元素: [1 2 3]
// 如果dst更大
dst2 := make([]int, 10)
n2 := copy(dst2, src)
fmt.Printf("复制了%d个元素: %v\n", n2, dst2)
// 复制了5个元素: [1 2 3 4 5 0 0 0 0 0]
// 实战:深拷贝切片
original := []int{10, 20, 30}
copied := make([]int, len(original))
copy(copied, original)
copied[0] = 999
fmt.Println("original:", original) // [10 20 30] 没变
fmt.Println("copied: ", copied) // [999 20 30]
// vs 直接赋值(共享底层数组)
shared := original
shared[0] = 999
fmt.Println("original:", original) // [999 20 30] 被改变了!
}
五、切片扩容机制深度解析
5.1 Go 1.18前后的扩容策略
切片的扩容机制是Go面试中最常见的问题之一。Go 1.18是一个重要分水岭:
| 版本 | 策略 |
|---|---|
| Go 1.17及以前 | oldCap < 1024 时容量翻倍;oldCap >= 1024 时增加25% |
| Go 1.18开始 | 更平滑的过渡:<256时翻倍;>=256时使用公式 newcap += (newcap + 3*threshold) / 4 |
Go 1.18+的扩容算法(简化版):
go
// 扩容的核心逻辑(伪代码)
func growslice(oldCap, newLen int) int {
newcap := oldCap
doublecap := oldCap * 2
if newLen > doublecap {
newcap = newLen
} else {
const threshold = 256
if oldCap < threshold {
newcap = doublecap
} else {
// 平滑过渡公式
for newcap < newLen {
newcap += (newcap + 3*threshold) / 4
}
// 内存对齐修正(略)
}
}
return newcap
}
5.2 扩容示例演示
go
package main
import "fmt"
func main() {
s := make([]int, 0)
// 追踪扩容过程
prevCap := cap(s)
for i := 1; i <= 2000; i++ {
s = append(s, i)
if cap(s) != prevCap {
fmt.Printf("len=%-5d cap=%-5d 扩容倍数=%.2f\n",
len(s), cap(s), float64(cap(s))/float64(prevCap))
prevCap = cap(s)
}
}
}
// Go 1.18+ 可能的输出:
// len=1 cap=1 扩容倍数=+Inf
// len=2 cap=2 扩容倍数=2.00 ← 翻倍
// len=3 cap=4 扩容倍数=2.00
// len=5 cap=8 扩容倍数=2.00
// ...
// len=256 cap=512 扩容倍数=2.00 ← 256以内翻倍
// len=513 cap=848 扩容倍数=1.66 ← 之后平滑增长
// len=849 cap=1280 扩容倍数=1.51
// len=1281 cap=1792 扩容倍数=1.40
// len=1793 cap=2304 扩容倍数=1.29
💡 实践启示 :如果能预估切片最终大小,用
make([]T, 0, expectedSize)预分配容量,可以避免多次扩容带来的内存分配和数据复制开销。这在性能敏感的场景中非常重要。
六、切片作为函数参数
切片的参数传递语义经常引起混淆------切片本身是值传递,但它包含指向底层数组的指针。
go
package main
import "fmt"
// 修改切片元素------会影响原切片(共享底层数组)
func modifyElement(s []int) {
s[0] = 999 // 通过ptr修改底层数组
}
// append操作------可能不影响原切片(取决于是否扩容)
func appendElement(s []int) {
s = append(s, 100) // 如果扩容,s指向新数组,原切片不受影响
fmt.Println("函数内append后:", s)
}
// 要修改切片本身(长度/容量),需要传指针
func appendAndReturn(s *[]int) {
*s = append(*s, 200)
}
func main() {
// 测试1:修改元素
s1 := []int{1, 2, 3}
modifyElement(s1)
fmt.Println("modifyElement后:", s1) // [999 2 3] ------ 受影响
// 测试2:append(可能触发扩容)
s2 := make([]int, 3, 3) // len=3, cap=3,一append就扩容
s2[0], s2[1], s2[2] = 1, 2, 3
appendElement(s2)
fmt.Println("appendElement后:", s2) // [1 2 3] ------ 不变!因为扩容了
// 测试3:append(不扩容的情况)
s3 := make([]int, 3, 10) // len=3, cap=10,append不扩容
s3[0], s3[1], s3[2] = 1, 2, 3
appendElement(s3)
fmt.Println("appendElement后(有容量):", s3) // [1 2 3] ------ 但函数内外len不同!
// 测试4:通过指针修改切片本身
s4 := []int{1, 2, 3}
appendAndReturn(&s4)
fmt.Println("appendAndReturn后:", s4) // [1 2 3 200]
}
⚠️ 关键记忆点 :
append是否影响调用者的切片,取决于是否触发扩容。这是Go中最容易出错的细节之一。安全做法:永远把append的返回值赋回原变量。
七、数组 vs 切片完整对比
| 维度 | 数组 [N]T |
切片 []T |
|---|---|---|
| 长度 | 固定,编译时确定 | 动态,运行时可变 |
| 类型 | [3]int ≠ [5]int |
所有[]int是同类型 |
| 值/引用 | 值类型(赋值=完整拷贝) | 引用语义(赋值=共享底层数组) |
| 内存布局 | 连续内存块 | 24字节头 + 底层数组(堆上) |
| 零值 | 所有元素为零值 | nil(len=0, cap=0, ptr=nil) |
| 比较 | == 逐元素比较(元素类型可比) |
只能与nil比较 |
| 作为参数 | 值拷贝(大数组昂贵) | 只拷贝24字节头(高效) |
| 创建方式 | var arr [5]int |
make([]int, 5), []int{...} |
| 使用频率 | 极少直接使用 | ⭐ 日常主力 |
| 适用场景 | 长度绝对不变的固定集合 | 几乎所有场景 |
go
// 数组可以直接比较
a1 := [3]int{1, 2, 3}
a2 := [3]int{1, 2, 3}
fmt.Println(a1 == a2) // true
// ❌ 切片不能比较(编译错误)
// s1 := []int{1,2,3}
// s2 := []int{1,2,3}
// fmt.Println(s1 == s2) // invalid operation: s1 == s2
// ✅ 切片只能与nil比较
var s []int
fmt.Println(s == nil) // true
// 如需比较两个切片,使用 reflect.DeepEqual 或手动遍历
八、常见陷阱与避坑指南
陷阱1:子切片共享底层数组(高频!)
这是Go切片中最经典的坑,无数Go开发者都踩过:
go
package main
import "fmt"
func main() {
// 场景:创建一个大切片,然后取一个"小窗口"
original := make([]int, 0, 1000000) // cap=100万
for i := 0; i < 100; i++ {
original = append(original, i)
}
// 取出前10个
sub := original[:10]
// 💣 问题:sub的底层数组仍然是那个100万元素的巨大数组
// 即使original本身不再被引用,GC也无法回收这100万元素的内存
fmt.Printf("sub: len=%d cap=%d\n", len(sub), cap(sub))
// sub: len=10 cap=1000000 ← 注意cap!
// ✅ 解决方案:使用copy创建独立切片
safe := make([]int, 10)
copy(safe, original[:10])
fmt.Printf("safe: len=%d cap=%d\n", len(safe), cap(safe))
// safe: len=10 cap=10 ← 干净的独立数据
// original可以安全回收了
}
陷阱2:append后原切片数据"消失"
go
package main
import "fmt"
func main() {
a := []int{1, 2, 3}
b := a // b和a共享底层数组
b = append(b, 4) // b可能扩容到新数组
// 如果扩容了,a和b指向不同的底层数组
b[0] = 999
fmt.Println("a:", a) // [1 2 3] ------ a不受影响
fmt.Println("b:", b) // [999 2 3 4]
}
陷阱3:range遍历中修改切片
go
// ❌ 在range中append可能导致混乱
s := []int{1, 2, 3}
for i, v := range s {
fmt.Println(i, v)
s = append(s, v*2)
}
// 输出:0 1 \n 1 2 \n 2 3
// range在开始时就确定了迭代次数,后续append不影响本次遍历
// ✅ 如果需要动态增长,用传统for
for i := 0; i < len(s); i++ {
s = append(s, s[i]*2)
if len(s) > 100 { break }
}
陷阱4:nil切片和空切片的行为差异
go
var nilSlice []int // nil
emptySlice := []int{} // 非nil,但len=0
// 大多数操作对两者无差别
fmt.Println(len(nilSlice)) // 0
fmt.Println(len(emptySlice)) // 0
nilSlice = append(nilSlice, 1) // ✅ 可以对nil切片append
// 但在JSON序列化中有差异
import "encoding/json"
j1, _ := json.Marshal(nilSlice) // null
j2, _ := json.Marshal(emptySlice) // []
避坑清单
| 陷阱 | 症状 | 解决方案 |
|---|---|---|
| 子切片cap过大 | 内存泄漏 | 用copy创建独立切片 |
| append不接收返回值 | 数据丢失 | 始终s = append(s, ...) |
| 切片共享底层 | 意外修改 | 需要独立数据时用copy |
| range中append | 行为不确定 | 用传统for循环 |
| nil切片JSON | 输出null而非\[\] | 用make([]T, 0)初始化 |
九、实战案例
案例1:去重函数
go
package main
import "fmt"
// 保持顺序的去重
func unique(s []int) []int {
seen := make(map[int]bool)
result := make([]int, 0, len(s))
for _, v := range s {
if !seen[v] {
seen[v] = true
result = append(result, v)
}
}
return result
}
func main() {
nums := []int{3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5}
fmt.Println(unique(nums)) // [3 1 4 5 9 2 6]
}
案例2:高效拼接字符串切片
go
package main
import (
"fmt"
"strings"
)
func main() {
words := []string{"Go", "是", "最好的", "语言", "!"}
// ❌ 低效:循环中使用 + 拼接
// result := ""
// for _, w := range words { result += w }
// ✅ 高效:使用 strings.Join
result := strings.Join(words, " ")
fmt.Println(result) // Go 是 最好的 语言 !
// 或者使用 strings.Builder
var builder strings.Builder
builder.Grow(100) // 预分配
for _, w := range words {
builder.WriteString(w)
}
fmt.Println(builder.String())
}
案例3:实现一个简单的Stack
go
package main
import "fmt"
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
func (s *Stack[T]) Peek() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
return s.items[len(s.items)-1], true
}
func (s *Stack[T]) Size() int {
return len(s.items)
}
func main() {
stack := Stack[string]{}
stack.Push("🍎")
stack.Push("🍌")
stack.Push("🍊")
fmt.Println("大小:", stack.Size()) // 3
if top, ok := stack.Peek(); ok {
fmt.Println("栈顶:", top) // 🍊
}
for item, ok := stack.Pop(); ok; item, ok = stack.Pop() {
fmt.Println("弹出:", item)
}
// 弹出: 🍊
// 弹出: 🍌
// 弹出: 🍎
}
十、小结与预告
📝 核心知识点回顾
| 知识点 | 核心内容 |
|---|---|
| 数组 | 固定长度,长度是类型一部分,值类型,完整拷贝 |
| 切片结构 | ptr + len + cap,24字节头,引用底层数组 |
| 创建方式 | 字面量、make、从数组/切片截取 |
| 截取规则 | [low:high:max],左闭右开,cap=cap(原)-low |
| append | 超过cap触发扩容,必须接收返回值 |
| copy | 取min(len(src),len(dst)),深拷贝 |
| 扩容策略 | 1.18前:<1024翻倍,>=1024增25%;1.18后更平滑 |
| 共享底层 | 子切片与原切片共享底层数组,注意内存泄漏 |
| 参数传递 | 切片本身值传递,但指向共享底层数组 |
🤔 互动问题
- 为什么Go设计者要让
[3]int和[5]int是不同类型?这带来了哪些好处和困扰? - 在实际项目中,你是否遇到过因子切片共享底层数组导致的内存泄漏?是如何发现的?
- 如果你来设计扩容策略,你会选择固定的倍增因子还是Go 1.18后的平滑方案?为什么?
📖 下篇预告
下一篇我们将学习另一个核心复合类型------Map(映射/字典)。Map的增删改查、comma ok模式、无序遍历、并发安全问题,以及如何用map实现Set------这些知识点将让你的Go数据处理能力再上一个台阶!
📚 参考资料
- Go官方博客 - Go Slices: usage and internals
- Go官方文档 - Effective Go: Slices
- Go语言规范 - Slice types
- Go Runtime源码 - growslice
💡 学习建议 :切片是Go中最核心的数据结构,理解其底层结构(ptr/len/cap)是写出正确、高效Go代码的前提。强烈建议读者自己写代码实验append的扩容行为,并用
fmt.Printf("%p", s)观察底层数组地址变化。动手实践远比死记硬背有效!