【Golang】Slice切片学习+实验代码

时间:2024-11-06 15:33:30

一、数组与切片区别

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)
}

结果