一、数组与切片区别
1.声明方式:
数组:需要指定数组的长度,例如 var arr [5]int。
切片:不需要指定长度,例如 var slice []int。
2.内存分配:
数组:声明时分配固定大小的内存空间。
切片:声明时不分配内存,只是定义了一个引用,可以指向一个数组或另一个切片。
3.操作:
数组:不能直接增长或缩小。
切片:可以通过 append 函数动态增长,也可以通过切片操作(如 slice[1:3])来缩小。
切片是一种数据结构,切片不是数组,切片描述的是一块数组 array (指针)len (有效长度)cap
二、切片声明
var slice []int // 直接声明, slice 是一个空切片,没有任何元素,也没有指向任何数组。
slice := []int{1,2,3,4,5} // 字面量方式,创建了一个包含五个整数的切片,并赋值给 slice。
slice := make([]int, 5, 10) // make创建,创建了一个长度为5,容量为10的切片,并赋值给 slice make 函数是创建切片的常用方法,它接受三个参数:切片类型、长度和容量。
slice := array[1:5] // 截取下标的方式,通过切片操作 array[1:5] 来创建一个新的切片,这个切片包含 array 中从索引1到索引4的元素。
slice := *new([]int) // new 关键字创建了一个指向 []int 类型切片的指针,并解引用它来得到一个切片。这种方式不常用,因为 new 创建的是指针,而我们通常直接使用 make 或字面量来创建切片。
三、切片内部结构
type Slice struct {
Data uintptr
Len int
Cap int
}
当切片作为参数传递时,其实就是一个结构体的传递,因为Go语言参数传递只有值传递,传递一个切片就会浅拷贝原切片,但因为底层数据的地址没有变,所以在函数内对切片的修改,也将会影响到函数外的切片,举例:
func modifySlice(s []string) {
s[0] = "hello"
s[1] = "Golang"
fmt.Println("out slice: ", s)
}
func main() {
s := []string{"hi", "Golang"}
modifySlice(s)
fmt.Println("inner slice: ", s)
}
// 运行结果
out slice: [hello Golang]
inner slice: [hello Golang]
不过这也有一个特例,先看一个例子:
func appendSlice(s []string) {
s = append(s, "!!")
fmt.Println("out slice: ", s)
}
func main() {
s := []string{"hi", "Golang"}
appendSlice(s)
fmt.Println("inner slice: ", s)
}
// 运行结果
out slice: [hi Golang!!]
inner slice: [hi Golang]
因为切片发生了扩容,函数外的切片指向了一个新的底层数组,所以函数内外不会相互影响,因此可以得出一个结论,当参数直接传递切片时,如果指向底层数组的指针被覆盖或者修改(copy、重分配、append触发扩容),此时函数内部对数据的修改将不再影响到外部的切片,代表长度的len和容量cap也均不会被修改。
参数传递切片指针就很容易理解了,如果你想修改切片中元素的值,并且更改切片的容量和底层数组,则应该按指针传递。
四.range遍历切片注意
Go语言提供了range关键字用于for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素,有两种使用方式:
for k,v := range _ { }
for k := range _ { }
第一种是遍历下标和对应值,第二种是只遍历下标,使用range遍历切片时会先拷贝一份,然后在遍历拷贝数据:
s := []int{1, 2}
for k, v := range s {
}
//会被编译器认为是
for_temp := s
len_temp := len(for_temp)
for index_temp := 0; index_temp < len_temp; index_temp++ {
value_temp := for_temp[index_temp]
_ = index_temp
value := value_temp
}
不知道这个知识点的情况下很容易踩坑,例如下面这个例子:
package main
import (
"fmt"
)
type user struct {
name string
age uint64
}
func main() {
u := []user{
{"张三", 23},
{"李四", 19},
}
for i := range u {
if u[i].age == 18 {
u[i].age = 20
}
}
// 打印修改后的切片
fmt.Println(u)
fmt.Println("Hello, World!")
}
//
[{张三 23} {李四 19}]
Hello, World!
因为使用range遍历切片u,变量v是拷贝切片中的数据,修改拷贝数据不会对原切片有影响。
五、扩容策略
切片在扩容时会进行内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于老 slice 容量的 2倍或者1.25倍,当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。
六、实验
package main
import "fmt"
func main() {
// 浅拷贝示例
fmt.Println("Shallow Copy Example:")
slice1 := []int{1, 2, 3}
slice2 := slice1[:] // 使用[:]进行浅拷贝
fmt.Println("Original slice:", slice1)
fmt.Println("Shallow copied slice:", slice2)
// 修改原始切片
slice1[0] = 100
fmt.Println("Modified original slice:", slice1)
fmt.Println("Shallow copied slice after modification:", slice2) // 浅拷贝后的切片也发生了变化
// 深拷贝示例
fmt.Println("\nDeep Copy Example:")
slice3 := make([]int, len(slice1))
copy(slice3, slice1) // 使用copy进行深拷贝
fmt.Println("Original slice:", slice1)
fmt.Println("Deep copied slice:", slice3)
// 修改原始切片
slice1[0] = 200
fmt.Println("Modified original slice:", slice1)
fmt.Println("Deep copied slice after modification:", slice3) // 深拷贝后的切片不受影响
// 大小切片拷贝代价对比
fmt.Println("\nCost of Copying Slices of Different Sizes:")
param1 := make([]int, 100)
param2 := make([]int, 100000000)
// 浅拷贝大切片和小切片
smallShallowCopy := param1[:]
largeShallowCopy := param2[:]
// 深拷贝大切片和小切片
smallDeepCopy := make([]int, len(param1))
copy(smallDeepCopy, param1)
largeDeepCopy := make([]int, len(param2))
copy(largeDeepCopy, param2)
// 展示深拷贝和浅拷贝的结果
fmt.Println("Small shallow copy:", smallShallowCopy)
fmt.Println("Large shallow copy:", largeShallowCopy)
fmt.Println("Small deep copy:", smallDeepCopy)
fmt.Println("Large deep copy:", largeDeepCopy)
}
结果