# 切片(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，容量为5，int类型的切片
```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{}) {
    // boxed 一下，再传递
    // 值传递，容器修改到副本，而不是对象本身
    handle_one(&obj)
}

func HandleRequest([]*any{}) {
    // box 给下去就行了
    handle_one(obj)
}
```

请将代码写在 `main.go` 文件中，并运行测试。
