go20/day02/slice/README.md
yumaojun03 2ba0062910 ```
docs(slice): 更新 README 文档

添加了关于切片总结部分的内容,补充了切片在 Go 语言中的特性和使用优势的说明。

新增第七个练习题目,包含问题代码分析任务,要求分析两个版本函数参数的差异并给出最优版本,
涉及指针和切片参数传递的相关知识点。
```
2026-01-11 17:00:21 +08:00

265 lines
7.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 切片(slice)
![alt text](image.png)
Go中的slice依赖于数组它的底层就是数组所以数组具有的优点, slice都有。 且slice支持可以通过append向slice中追加元素长度不够时会动态扩展通过再次slice切片可以得到得到更小的slice结构可以迭代、遍历等
基于数组的 复合数据结构
```go
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 数组指针
cap int // 容量
len int // 长度
}
```
每一个slice结构都由3部分组成
+ 容量(capacity): 即底层数组的长度表示这个slice目前最多能扩展到这么长
+ 长度(length)表示slice当前的长度即当前容纳的元素个数
+ 数组指针(array): 指向底层数组的指针
比如创建一个长度为3容量为5int类型的切片
```go
s := make([]int, 3, 4)
fmt.Println(a, len(s), cap(s)) // [0 0 0] 3 5
```
![alt text](image-1.png)
## 切片与数组
为了灵活(不需要制定长度, 自动扩容)
```go
func main() {
// 1. 声明切片, 切片不是一个值是一个boxed结构体, array unsafe.Pointer // 数组指针
// 底层数组的长度: 容量10, 当前有几个元素3
slice1 := make([]int, 3, 5)
fmt.Println(slice1, len(slice1), cap(slice1))
// 底层数组的长度: 容量10, 当前有几个元素3
slice1 = append(slice1, 4, 5)
fmt.Println(slice1, len(slice1), cap(slice1))
// 这里的容器, 不是硬性限制,是超过容量后,底层数组是自动扩容的(重要)
// 扩容: 一般是原来的2倍, 新申请一块更大的数组, 把老数据copy过去
slice1 = append(slice1, 6)
fmt.Println(slice1, len(slice1), cap(slice1))
}
```
## 创建和初始化
**原理**:切片是动态数组,底层基于固定大小的数组,可以根据需要动态扩容。创建时可以指定长度和容量。
```go
// 使用 make 创建切片make([]类型, 长度, 容量)
s1 := make([]int, 3, 5) // 长度3容量5
fmt.Println(s1, len(s1), cap(s1)) // [0 0 0] 3 5
// 直接初始化
s2 := []int{1, 2, 3} // 长度和容量都是3
fmt.Println(s2, len(s2), cap(s2)) // [1 2 3] 3 3
// 从数组创建切片
arr := [5]int{1, 2, 3, 4, 5}
s3 := arr[1:4] // 从索引1到3不包括4
fmt.Println(s3, len(s3), cap(s3)) // [2 3 4] 3 4
```
## 切片访问
**原理**切片通过索引访问元素索引从0开始。与数组类似但更灵活。
```go
s := []int{10, 20, 30, 40, 50}
// 通过索引访问元素
fmt.Println(s[0]) // 10
fmt.Println(s[2]) // 30
// 修改元素
s[1] = 25
fmt.Println(s) // [10 25 30 40 50]
```
## nil和空切片
**原理**nil切片表示未初始化的切片长度和容量都为0。空切片是已初始化的切片但不包含元素。两者在行为上略有不同。
```go
// nil 切片:未初始化的切片
var s1 []int
fmt.Println(s1 == nil) // true
fmt.Println(len(s1), cap(s1)) // 0 0
// 空切片:已初始化的空切片
s2 := []int{}
s3 := make([]int, 0)
fmt.Println(len(s2), cap(s2)) // 0 0
fmt.Println(len(s3), cap(s3)) // 0 0
```
## 往切片中添加元素
**原理**使用append函数添加元素。如果容量不足会自动扩容底层数组通常容量翻倍增长。
```go
s := []int{1, 2}
fmt.Println(s, len(s), cap(s)) // [1 2] 2 2
// 使用 append 添加元素
s = append(s, 3)
fmt.Println(s, len(s), cap(s)) // [1 2 3] 3 4 (容量自动扩容)
// 添加多个元素
s = append(s, 4, 5)
fmt.Println(s) // [1 2 3 4 5]
```
![alt text](image-2.png)
## 遍历切片
**原理**可以使用传统for循环或range关键字遍历。range返回索引和值更简洁。
```go
s := []string{"apple", "banana", "cherry"}
// 方法1使用索引
for i := 0; i < len(s); i++ {
fmt.Println(i, s[i])
}
// 方法2使用 range推荐
for index, value := range s {
fmt.Println(index, value)
}
```
## slice是引用类型
**原理**:切片是引用类型,赋值时复制切片头,但共享底层数组。修改一个会影响另一个。
```go
s1 := []int{1, 2, 3}
s2 := s1 // s2 指向同一个底层数组
s2[0] = 99
fmt.Println(s1) // [99 2 3] s1 也被修改了
fmt.Println(s2) // [99 2 3]
```
![alt text](image-3.png)
## 切片拷贝
**原理**使用copy函数进行深拷贝创建独立的数据副本。修改拷贝后的切片不会影响原切片。
```go
s1 := []int{1, 2, 3}
s2 := make([]int, len(s1))
copy(s2, s1) // 拷贝元素到 s2
s2[0] = 99
fmt.Println(s1) // [1 2 3] s1 不变
fmt.Println(s2) // [99 2 3]
```
![alt text](image-4.png)
## 通过切片创建新的切片
**原理**切片操作创建新的切片视图共享底层数组。新切片的长度是high-low容量是原容量减去low。
```go
s := []int{0, 1, 2, 3, 4, 5}
// 创建子切片s[low:high] (不包括high) [start: end)
sub1 := s[1:4] // [1, 2, 3]
fmt.Println(sub1)
// 省略索引
sub2 := s[:3] // [0, 1, 2]
sub3 := s[2:] // [2, 3, 4, 5]
sub4 := s[:] // [0, 1, 2, 3, 4, 5] 复制整个切片
```
![alt text](image-6.png)
## 切片作为函数参数 (重点: 90%)
**原理**:切片作为参数传递时,是引用传递,函数内修改会影响调用者。高效但需注意副作用。
```go
// 冒泡排序: 值传递
// 返回一个新的切片(排序的结果)
func MySort(arr []int) []int {
// 冒泡排序
for i := 0; i < len(arr)-1; i++ {
for j := 0; j < len(arr)-1-i; j++ {
if arr[j] > arr[j+1] {
// 值交换: a, b = b, a
arr[j], arr[j+1] = arr[j+1], arr[j]
}
}
}
return arr
}
// 冒泡排序: 引用传递
// 直接修改传入的切片(在原切片上进行排序)
func MySortV2(arr *[]int) {
// 冒泡排序
for i := 0; i < len(*arr)-1; i++ {
for j := 0; j < len(*arr)-1-i; j++ {
if (*arr)[j] > (*arr)[j+1] {
// 值交换: a, b = b, a
(*arr)[j], (*arr)[j+1] = (*arr)[j+1], (*arr)[j]
}
}
}
}
```
## 总结
切片是 Golang 中比较有特色的一种数据类型,既为我们操作集合类型的数据提供了便利的方式,是又能够高效的在函数间进行传递,因此在代码中切片类型被使用的相当广泛
## 作业
请同学们完成以下切片练习题以巩固对Go语言切片的理解
1. **创建和初始化切片**
创建一个字符串切片,包含"Go", "Python", "Java",并打印长度和容量。
2. **切片操作**
给定切片[]int{1,2,3,4,5}创建子切片包含第2到第4个元素不包括第4个并打印结果。
3. **添加元素**
从空切片开始使用append添加5个整数观察长度和容量的变化。
4. **遍历切片**
遍历一个整数切片,计算所有元素的和。
5. **切片拷贝**
创建一个切片,拷贝到另一个切片,修改原切片,验证拷贝是否独立。
6. **函数参数**
编写一个函数,接受字符串切片作为参数,将所有字符串转换为大写并返回新切片, 需要实现2个版本 1.一个值版本, 2. 指针版本。
7. **问题代码**
分析下面2个版本的差异, 给出最优版本
```go
requests := []string{"a", "b", "c"}
func HandleRequest(*[]any{})
func HandleRequest([]*any{})
```
请将代码写在 `main.go` 文件中,并运行测试。