【Golang】Slice切片学习+实验代码
一、数组与切片区别
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 uintptrLen intCap 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 := slen_temp := len(for_temp)for index_temp := 0; index_temp < len_temp; index_temp++ {value_temp := for_temp[index_temp]_ = index_tempvalue := value_temp}
不知道这个知识点的情况下很容易踩坑,例如下面这个例子:
package mainimport ("fmt"
)type user struct {name stringage 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 mainimport "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] = 100fmt.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] = 200fmt.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)
}
结果