第一题
package main
import (
"fmt"
)
func main() {
var array [10]int
var slice = array[5:6]
fmt.Println("lenth of slice: ", len(slice))
fmt.Println("capacity of slice: ", cap(slice))
fmt.Println(&slice[0] == &array[5])
}
lenth of slice: 1
capacity of slice: 5
true
程序解释:main函数中定义了一个10个长度的整型数组array,然后定义了一个切片slice,切取数组的第6个元素,最后打印slice的长度和容量,判断切片的第一个元素和数组的第6个元素地址是否相等。
参考答案 :slice跟据数组array创建,与数组共享存储空间,slice起始位置是array[5],长度为1,容量为5,slice[0]和array[5]地址相同。
第二题
package main
import (
"fmt"
)
func AddElement(slice []int, e int) []int {
return append(slice, e)
}
func main() {
var slice []int
slice = append(slice, 1, 2, 3)
fmt.Println(len(slice)) //3
fmt.Println(cap(slice)) //3
newSlice := AddElement(slice, 4)
fmt.Println(len(newSlice)) //4
fmt.Println(cap(newSlice)) //6
fmt.Println(&slice[0] == &newSlice[0]) //false
}
程序解释 :第一次使用append函数向空切片中添加三个元素,slice长度是3容量是3,
然后执行AddElement函数中执行的是浅拷贝,返回了一个扩容后长度为4,容量为6的切片并赋给newSlice,所以原slice和newSlice的地址是不一样的。
这里newSlice的地址值其实是变了两次
第二题变形
package main
import (
"fmt"
)
func main() {
var slice []int
slice = append(slice, 1, 2, 3)
fmt.Println(len(slice)) //3
fmt.Println(cap(slice)) //3
newSlice := append(slice, 4)
fmt.Println(len(newSlice)) //4
fmt.Println(cap(newSlice)) //6
fmt.Println(&slice[0] == &newSlice[0]) //false
}
所以上面的newSlice和这里的newSlice的地址也是不一样的
第三题
package main
import "fmt"
func main() {
s := []int{1, 2}
s = append(s, 4, 5, 6)
fmt.Println(len(s))//5
fmt.Println(cap(s))//6
}
容量是6而不是8的原因
由于初始 s 的容量是2,现需要追加3个元素,所以通过 append 一定会触发扩容,并调用 growslice 函数,此时他的入参 cap 大小为2+3=5。通过翻倍原有容量得到 doublecap = 2+2,doublecap 小于 cap 值,所以在第一阶段计算出的期望容量值 newcap=5。在第二阶段中,元素类型大小 int 和 sys.PtrSize 相等,通过 roundupsize 向上取整内存的大小到 capmem = 48 字节,所以新切片的容量newcap 为 48 / 8 = 6 ,成功解释!
详细解释参考:
https://segmentfault.com/a/1190000040413412?sort=newest
第四题(切片扩容与类型有关)
package main
import "fmt"
func main() {
s := make([]int32, 0)
s = append(s, 1)
fmt.Println("cap s", cap(s))//2
s1 := make([]int, 0)
s1 = append(s1, 1)
fmt.Println("cap s1", cap(s1))//1
}
第五题
package main
import "fmt"
func modifySlice(s []int) {
s[0] = 100
s[1] = 101
fmt.Printf("1:%p\n", &s)
s = append(s, 200)
fmt.Printf("2:%p\n", &s)
fmt.Println(s)
}
func main() {
nums := []int{1, 2, 3, 4, 5}
fmt.Println("Before:", nums) // 输出: Before: [1 2 3 4 5]
fmt.Printf("Before:%p\n", &nums)
modifySlice(nums)
fmt.Println("After:", nums) // 输出: After: [100 2 3 4 5]
fmt.Printf("After:%p\n", &nums)
}
因为切片是引用类型所以作为参数去传递的时候在函数里面是能改变原切片的值,但是扩容的话相当于改变切片的值之后重新赋给了一个新变量,地址已经变了,所以不会有200
面试题
说一说切片中append()的扩容机制
Go切片的扩容机制还是比较复杂的,受到Go版本、操作系统、数据类型等因数,其机制都会有不同,要具体情况具体分析。
常用的说法是以下两点
1、当需要的容量超过原切片容量的两倍时,会使用需要的容量作为新容量。
2、当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。
当Go的runtime分配内存的时候,会调用roundupsize,取整内存值(例子是上面的第三题)
不同的切片类型,扩容值可能是不同的(例子是上面第四题)
当go的版本在1.18之后时,growslice改了
1024变成了256,公式也改了,newcap += newcap / 4变成了newcap += (newcap + 3*threshold) / 4
简单说一下Go里面Slice的实现,底层结构
type slice struct {
ptr unsafe.Pointer // 指向底层数组的指针
len int // 切片的长度
cap int // 切片的容量
}
ptr
是一个指向底层数组的指针,len
表示切片的长度,cap
表示切片的容量
slice
的底层实现采用了指针的方式,这使得多个slice
可以共享同一个底层数组。在切片复制时,只是复制了slice
结构体本身,而不是底层数组。这意味着,在不修改底层数组的情况下,可以通过多个slice
来访问同一个底层数组的不同部分。
总的来说,它的底层实现采用了指针和动态数组的方式.
扩容的具体过程怎么样的,你只说了容量的扩容,比如说扩容涉及到元素的拷贝,需要做数据拷贝吗?
在对一个数据结构进行扩容时,一般需要执行以下步骤:
- 申请新的内存空间。这个新的内存空间的大小要比原来的空间大,通常会选择原来空间大小的2倍或4倍等等。
- 将原来内存空间中的数据拷贝到新的内存空间中。这个过程通常需要遍历原来内存空间中的所有元素,然后将它们复制到新的内存空间中。这个过程涉及到数据的拷贝,因此可能会导致一些性能损失。
- 释放原来的内存空间。一旦数据成功地拷贝到新的内存空间中,就可以释放原来的内存空间,以便其他应用程序可以使用它。
需要注意的是,扩容操作不仅仅是涉及到容量的扩容,同时也需要考虑到数据的拷贝问题。如果不进行数据的拷贝,就无法将原来的数据结构的所有元素都存储到新的内存空间中,从而导致数据丢失或不完整。因此,数据拷贝是扩容操作中不可避免的一个步骤。