优秀的编程知识分享平台

网站首页 > 技术文章 正文

深入理解Golang Slice 深入理解计算机系统 电子书

nanyue 2024-12-17 15:30:44 技术文章 4 ℃


在面试Golang工程师都会被面试官问到的一些关于slice原理的一些面试题:

  1. slice的底层实现原理?
  2. 数组和slice的区别是什么?
  3. slice的深拷贝和浅拷贝
  4. slice的扩容机制是怎么样的?

针对以上的面试题,我们需要深入理解slice的一些原理

slice的底层实现原理

slice是无固定长度的数组且使用之前要先分配内存,slice的底层结构是一个结构体有3个属性;

type slice struct {
    array unsafe.Pointer   // 8bytes
    len int   // 8bytes
    cap int   // 8bytes
}
  • array:表示一个指向一个数组的指针,数据存储在这个指针指向的数组上;
  • len:slice的长度;
  • cap:slice的容量,同时也是底层数组的长度;

由此我们可以看出slice是一个引用类型,slice指向底层的数组,声明slice可以像声明数组一样,只是slice的长度是可变的。

slice跟数组的区别

数组

  • 数组初始化

数组在初始化之前必须指定大小和初值,但是我们可以使用go的语法糖来灵活初始化数组,例如:使用...来自动获取长度;未指定值得时候用0赋予初始值。

var arr [5]int   //声明了一个大小为5的数组,默认初始化值为[0,0,0,0,0]

arr := [5]int{1}  //声明并初始化了一个大小为5的数组的第一个元素,初始化后值为[1,0,0,0,0]
arr := [...]int{1,2,3,4} //通过...自动获取数组长度,根据初始化的值的数量将大小初始化为4,初始化后值为[1,2,3,4]
arr := [...]int{2:1}  // 指定序号为2的元素的值为1,通过...自动获取长度为3,初始化后值为[0 0 1]
  • 数组作为函数参数

数组作为函数参数时,必须指定参数数组的大小,且传入的数组大小必须与指定的大小一致,数组为按值传递的,函数内对数组的值的改变不影响初始数组

package main


import "fmt"


func PrintArray(arr [4]int){
    arr[1] = 5
    fmt.Println(arr)
}

func main(){
    mainArray := [...]int{1,2,3,4}
    PrintArray(mainArray)  // [1 5 3 4]
    fmt.Println(mainArray)  // [1 2 3 4]
}

切片slice

  • 切片初始化

slice在初始化时需要初始化指针,长度和容量,容量未指定时将自动初始化为长度的大小。可以通过直接获取数组的引用、获取数组slice的切片构建或是make函数初始化数组

注意:如果通过slice初始化slice,因为两个slice都是指向同一个数组,所以改变某一个slice里面的值都会导致两个slice的值都改变。

var slice1 []int{1,2,3}

arr := [5]int{1,2,3,4,5}
slice2 := arr[0:3] // 通过数组来初始化切片,值为[1,2,3],长度为3,容量为5

slice3 := make([]int, 3,5) // 通过make函数来初始化切片,值为[0,0,0],长度为3,容量为5
  • 切片作为函数参数

slice的参数传递是引用传递,在被调用函数修改元素的值,同时也是在修改调用方的slice的元素。

package main

import "fmt"

func modifySlice(s []int) {
    s[0] = 10
    fmt.Println("In modifySlice function, slice values is:", s)

}
func main() {
    s := []int{1,2,3,4,5}
    modifySlice(s)
    fmt.Println("In main, slice values is:",s)

}

总结:

  • 切片是指针类型,数组是值类型
  • 数组的长度是固定的,而切片长度可以任意调整(切片是动态的数组)
  • 数组只有长度一个属性,而切片比数组多了一个容量(cap)属性
  • 切片的底层也是使用数组实现的

slice的深拷贝跟浅拷贝

深拷贝:拷贝的是数据本身,创造一个样的新对象,新创建的对象与原对象不共享内存,新创建的对象在内存中开辟一个新的内存地址,新对象值修改时不会影响原对象值,既然内存地址不同,释放内存地址时,可分别释放。在Go中值类型的数据默认赋值操作都是深拷贝比如:数组,整型,字符串,结构体,浮点型,布尔型,如果引用类型的数据想通过深拷贝就需要copy函数来完成。


浅拷贝:拷贝的是数据地址,只复制指向的对象的指针,此时新对象和老对象指向的内存地址是一样的,新对象值修改时老对象也会变化。释放内存地址时,同时释放内存地址。Go中引用类型的数据,默认全部都是浅拷贝,Slice、Map等。

  • Go深拷贝
package main

improt "fmt"

func main() {
    slice1 := []int{1, 2, 3, 4, 5}
    slice2 := make([]int, 5, 5)
    // 深拷贝
    copy(slice2, slice1)                   
    fmt.Println(slice1, len(slice1), cap(slice1)) 
    // [1 2 3 4 5] 5 5
    fmt.Println(slice2, len(slice2), cap(slice2)) 
    // [1 2 3 4 5] 5 5
    slice1[1] = 100                        
    fmt.Println(slice1, len(slice1), cap(slice1)) 
    // [1 100 3 4 5] 5 5
    fmt.Println(slice2, len(slice2), cap(slice2)) 
    // [1 2 3 4 5] 5 5
}
  • Go浅拷贝

注意:在复制 slice 切片的时候,slice切片 中数组的指针也会被复制了,在触发扩容逻辑之前,两个 slice 指向的是相同的数组,触发扩容逻辑之后指向的就是不同的数组了。

package main

import "fmt"

func main() {
	slice1 := []int{1, 2, 3, 4 ,5 ,6 }
	// 浅拷贝(注意赋值操作对于引用类型是浅拷贝,对于值类型是深拷贝)
	slice2 := slice1
	fmt.Printf("%p\n", slice1) // 0xc00001e1b0
	fmt.Printf("%p\n", slice2) // 0xc00001e1b0
	// 同时改变两个数组,这时就是浅拷贝,未扩容时,修改 slice1 的元素之后,slice2 的元素也会跟着修改
	slice1[0] = 10
	fmt.Println(slice1, len(slice1), cap(slice1))
	// [10 2 3 4 5 6] 6 6
	fmt.Println(slice2, len(slice2), cap(slice2))
	// [10 2 3 4 5 6] 6 6
	// 注意下:扩容后,slice1和slice2不再指向同一个数组,修改 slice1 的元素之后,slice2 的元素不会被修改了
	slice1 = append(slice1, 5, 6, 7, 8)
	slice1[0] = 11
	// 这里可以发现,slice1[0] 被修改为了 11, slice1[0] 还是10
	fmt.Println(slice1, len(slice1), cap(slice1))
	// [11 2 3 4 5 6 5 6 7 8] 10 12
	fmt.Println(slice2, len(slice2), cap(slice2))
	[10 2 3 4 5 6] 6 6
}

slice切片的扩容机制是什么?

当slice的长度已经等于容量的时候,再使用append()给slice追加元素,会自动扩展底层数组的长度。这时候就会发生切片的扩容。

注意:

  • 在原容量扩大两倍还要小于扩容后的容量时,预估容量就是扩容后的,当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍
  • append 函数调用 growslice 函数获取所需要的内存,这称为扩容,扩容会改变元素原来的位置。
package main

import "fmt"

func main() {
	a := make([]int, 20)
	b := make([]int, 42)
	a = append(a, b...)
	fmt.Println(len(a), cap(a))  // 62
}
最近发表
标签列表