1.数组和切片的关系和区别(跟谁学,京东,伴鱼,腾讯+3,小米+3,百度,深信服,知乎,触宝,微步,北京神州,度小满,腾讯音乐,早安科技)
2.如何把数组转换成一个切片(跟谁学)
3.slice用copy和左值进行初始化的区别 (有色)--------copy的用法在下面
4.一个函数传参一个slice,先append在赋值和另外一个函数先赋值在append, 那个会发生变化?(字节)
5.slice分配在堆上还是栈上 (京东)
6.切片的比较,深拷贝的问题(北京合链)
7.make一个slice后返回的是指针吗? map呢?(字节)
地鼠文档www.topgoer.cn/docs/golang…
彻底理解Golang Slicemp.weixin.qq.com/s/Rqi_nSbw6…
一.数组和切片的区别(8个问题)
对应问题
1.数组和切片的关系和区别(跟谁学,京东,伴鱼,腾讯+3,小米+3,百度,深信服,知乎,触宝,微步,北京神州,度小满,腾讯音乐,早安科技)
2.如何把数组转换成一个切片(跟谁学)
3.slice用copy和左值进行初始化的区别 (有色)--------copy的用法在下面
4.一个函数传参一个slice,先append在赋值和另外一个函数先赋值在append, 那个会发生变化?(字节)
5.slice分配在堆上还是栈上 (京东)
6.切片的比较,深拷贝的问题(北京合链)
7.make一个slice后返回的是指针吗? map呢?(字节)
1.数组和切片初始化的区别(问题1,2,3)
1.1数组的初始化
数组的初始化一共有两种方式
(1):采用的是 [x]type{a,b,c}
(2):采用的是[...]type{a,b,c}(自动计算数组的长度)
例子如下
//在定义的时候就确定长度 [x]type{a,b,c}
var arr0 = [3]int{1, 2, 3}
fmt.Println(arr0)
//在定义的时候省略长度 [...]type{a,b,c}
var arr1 = [...]int{1, 2, 3}
fmt.Println(arr1)
//在定义的时候制定下标对应的值
var arr2 = [...]int{3: 4, 4: 5}
fmt.Println(arr2)
//自定义结构的数组
var arr3 = [...]struct {
name string
age uint8
}{
{"xiaoming", 8},
{"xiaojin", 16},
}
fmt.Println(arr3)
1.2切片的初始化
切片的初始化方法就多了,一共有四种初始化的方式
(1): var arr1 [] int
(2):make([]type,len)
(3):make([]type,len,cap)
(4):从数组上面
//方法1 直接声明 (直接声明的话将是nil,也被成为nil切片)
var arr1 []int
if arr1 == nil {
fmt.Println("空")
} else {
fmt.Println("非空")
}
fmt.Println(len(arr1))
fmt.Println(cap(arr1))
/* 方法1 打印结果
arr1空
0
0
*/
// 方法2 使用 :=
arr2 := []int{}
if arr2 == nil {
fmt.Println("空")
} else {
fmt.Println("非空")
}
fmt.Println(len(arr2))
fmt.Println(cap(arr2))
/* 方法2 打印结果
arr2非空
0
0
*/
// 方法3 使用 make 创建 make([]type,len,cap)
// 3.1 make([]type,len) 省略cap 此时cap默认等于len
var arr3_1 = make([]int, 5)
if arr3_1 == nil {
fmt.Println("arr3_1空")
} else {
fmt.Println("arr3_1非空")
}
fmt.Println(len(arr3_1))
fmt.Println(cap(arr3_1))
//3.2 make([]type,len,cap)
var arr3_2 = make([]int, 0, 5)
if arr3_2 == nil {
fmt.Println("arr3_2空")
} else {
fmt.Println("arr3_3非空")
}
fmt.Println(len(arr3_2))
fmt.Println(cap(arr3_2))
/* 方法3 打印结果
arr3_1非空
5
5
arr3_3非空
0
5
*/
// 方法4 从数组切片
arrTemp := [5]int{1, 2, 3, 4, 5}
arr4 := arrTemp[:]
if arr4 == nil {
fmt.Println("arr4空")
} else {
fmt.Println("arr4非空")
}
fmt.Println(len(arr4))
fmt.Println(cap(arr4))
/* 方法4 打印结果
arr4非空
5
5
*/
结论:只有第一种方法(var arr []int)定义切片的时候,切片为空,而且如果slice==nil,len,cap的结果都为0
需要注意的是从数组切片的方法中可以有三个参数,详细如下(来自地鼠文档)
2.数组和切片在函数传参中的区别(问题3,4,5,6,7)
对应问题
一个函数传参一个slice,先append在赋值和另外一个函数先赋值在append, 那个会发生变化?(字节)
slice分配在堆上还是栈上 (京东)
切片的比较,深拷贝的问题(北京合链)
make一个slice后返回的是指针吗? map呢?(字节)
Go语言中所有的传参都是值传递(传值),都是一个副本,一个拷贝。
Golang中引用类型:指针,slice,map,channel,接口,函数等。变量存放的是一个内存地址值,这个地址值指向的空间存的才是最终的值。内存通常在堆中分配,当没有任何变量引用这个地址时,改地址对应的数据空间就成为一个垃圾,通过GC回收
1.简单的数据像 int,float,bool,string这样的基本类型都属于值类型,传参到函数后,在函数中做出的修改就是对副本进行了修改,到了函数外面还是原来的值
2.复杂的数据结构一般都是用引用类型保存,虽然传进去的是副本,但是副本指针依然指向的是同一块空间,故对函数中引用类型的修改会修改函数外面的值,到了函数外面,将会发生改变
2.1数组的函数传参以及复制(值传递)
//数组中的复制,以及函数中的传递都是值赋值的
func TestArrayTrans(t *testing.T) {
arrayA := [2]int{100, 200}
var arrayB [2]int
arrayB = arrayA
fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)
//在函数中对arrayA进行修改
testArray(arrayA)
//函数中修改后,A还是原来的值
fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
/* 打印结果:
arrayA : 0xc00000a350 , [100 200]
arrayB : 0xc00000a360 , [100 200]
func Array : 0xc00000a390 , [100 200]
三个打印的地址都不同(说明数组的传递是值传递的,即每次都是复制和传参中都是开辟一个新的内存地址,并一一复制),这样如果数组的长度较大的时候将会消耗大量的空间
*/
}
func testArray(x [2]int) {
fmt.Printf("func Array : %p , %v\n", &x, x)
x[0] = 1
}
2.2切片的函数传参以及复制(引用传递)
//数组中的复制,以及函数中的传递都是引用赋值的
func TestSliceTrans(t *testing.T) {
arrayA := []int{100, 200}
var arrayB []int
//浅拷贝,只是拷贝了指针,两个指针指向的却是同一块地址,对arrayA的改变会影响到arrayB
//arrayB = arrayA
//深拷贝,开辟了一个新的空间,两个指针指向的是不同的地址,此时对arrayA的改变不会影响到arrayB
//注意:copy函数取决于两者中最短的那个slice,如果没有下面的两行append,arrayB的长度是0,那么arrayB的打印结果将为[]
//arrayB = append(arrayB, 1)
//arrayB = append(arrayB, 1)
//copy(arrayB, arrayA)
fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)
testSlice(arrayA)
//函数中修改后,A还是原来的值
fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
fmt.Printf("arrayA : %p , %v\n", &arrayB, arrayB)
/* 打印结果
arrayA : 0xc0000040d8 , [100 200]
arrayB : 0xc0000040f0 , [100 200]
func Array : 0xc000004138 , [100 200]
arrayA : 0xc0000040d8 , [1 200]
arrayA : 0xc0000040f0 , [1 200]
slice是引用类型的传递,地址虽然不同,但是只是两个不同地址的指针,指针指向的是同一块内存地址,在函数中进行的改变会影响到函数外面的结果,而且拷贝也只是拷贝了一份指针,两个指针指向同一块地址,对arrayA的修改,会影响到arrayB的值
因此引用类型的拷贝一般又特定的函数来进行深拷贝,如slice中使用copy(dst,src)函数
*/
}
2.3数组函数传参的小坑
[4]int和[3]int不被视为同一种类型,编译不通过
func TestArrayFuncParameter(t *testing.T) {
numbersArray := [3]int{1, 2, 3}
//编译不通过
SumForArray(numbersArray)
}
func SumForArray(numbers [4]int) int {
sum := 0
for i := 0; i < 5; i++ {
sum += numbers[i]
}
return sum
}
3.数组和切片的关系
func TestRelation(t *testing.T) {
arr := [5]int{1, 2, 3, 4, 5}
slice := arr[1:3]
fmt.Println(len(slice), cap(slice))
}
示意图如下
结论:
数组
1.数组固定长度
2.数组需要指定大小,不指定的话可以通过[...]type,将进行自动推算出大小,数组长度是数据类型的一部分,当函数需要传参数[4]int 的时候,如果传入的参数是[3]int 编译不通过
3.数组是通过值传递的
切片
1.切片可以改变长度
2.数组不需要指定大小
3.切片是引用传递
二.silce切片的操作(创建,增加,删除,查找,修改,截取)
对应问题
go切片如何删除数据(大疆)
0.创建
func TestInit(t *testing.T) {
//直接声明,需要注意初始化中只有此时的slice为空
var slice1 []int
fmt.Println(len(slice1), cap(slice1))
// :=
slice2 := []int{}
fmt.Println(len(slice2), cap(slice2))
// make([]int,len,cap)
var slice3 = make([]int, 0, 0)
fmt.Println(len(slice3), cap(slice3))
//从数组截取
arrTemp := [5]int{1, 2, 3, 4, 5}
arr4 := arrTemp[:]
fmt.Println(len(arr4))
fmt.Println(cap(arr4))
}
1.增加
//在512之前都是两倍的增长
func TestCap(t *testing.T) {
var r = 10000
arr := make([]int, 0, 1)
c := cap(arr)
for i := 0; i < r; i++ {
arr = append(arr, 1)
if n := cap(arr); n != c {
fmt.Printf("%d->%d %f\n", i, n, float64(n)/float64(i))
c = n
}
}
}
/* 打印结果
1->2 2.000000
2->4 2.000000
4->8 2.000000
8->16 2.000000
16->32 2.000000
32->64 2.000000
64->128 2.000000
128->256 2.000000
256->512 2.000000
512->848 1.656250
848->1280 1.509434
1280->1792 1.400000
1792->2560 1.428571
2560->3408 1.331250
3408->5120 1.502347
5120->7168 1.400000
7168->9216 1.285714
9216->12288 1.333333
*/
详细的扩容需要看底层
2.删除
//切片的删除操作
func TestDelete(t *testing.T) {
slice := []int{1, 2, 3, 4, 5}
fmt.Println(slice)
// 删除最后一个
slice = slice[:len(slice)-1]
fmt.Println(slice)
//删除第2个
slice = append(slice[:1], slice[2:]...)
fmt.Println(slice)
}
3.查找
直接通过下标访问
v := slice[i]
4.修改
直接通过下标修改
slice[i] = 3
4.截取
三.slice切片的底层
对应问题
切片底层(跟谁学,好未来,伴鱼,知乎,七牛+3,京东,哔哩哔哩,腾讯+1,小米,字节,Aibee,网易)
slice,len,cap,共享,扩容(腾讯)
append底层 (七牛)
slice内存泄漏分析 (知乎)
1.切片的数据结构
由图可以看到切片的底层实际上是一个数据结构,包含指向数组的指针,len,cap
len是切片可以访问的长度,当用for遍历的时候,打印出来的个数就是len的长度
cap最大可以是底层数组的容量-array指针的位置,当底层数组容量不够的时候,将进行扩容
并且两个切片还允许指向同一个底层数组,这个时候如果对一个切片进行操作,会改变另外一个切片的值 (共享存储空间)
func TestTwoSliceChange(t *testing.T) {
array := [6]int{10, 20, 30, 40, 50, 60}
sliceA := array[2:5:5]
sliceB := array[1:3:5]
fmt.Println(sliceA)
fmt.Println(sliceB)
sliceA[0] = 999
fmt.Println(sliceA)
fmt.Println(sliceB)
/*打印结果
[30 40 50]
[20 30]
[999 40 50]
[20 999]
*/
}
2.切片的扩容(1.18)
go的切片扩容机制(小米+1, 腾讯,知乎,字节,伴鱼)
在以前的版本中:
- 如果新申请容量比两倍原有容量大,那么扩容后容量大小 等于 新申请容量
- 如果原有 slice 长度小于 1024, 那么每次就扩容为原来的 2 倍
- 如果原 slice 大于等于 1024, 那么每次扩容就扩为原来的 1.25 倍
在1.18的版本中:
- 如果新申请容量比两倍原有容量大,那么扩容后容量大小等于新申请容量
- 如果原有slice长度小于256,那么每次就扩容为原来的2倍
- 如果原有slice大于等于256,那么每次扩容就扩为原来的1.25倍+3/4*256(也就是1.25倍再加192)
测试代码如下
//测试cap的变化规律
//在512之前都是两倍的增长
func TestCap(t *testing.T) {
var r = 10000
arr := make([]int, 0, 1)
c := cap(arr)
for i := 0; i < r; i++ {
arr = append(arr, 1)
if n := cap(arr); n != c {
fmt.Printf("%d->%d %f倍\n", i, n, float64(n)/float64(i))
c = n
}
}
}
扩容 源代码在 go/src/slice.go中
func growslice(et *_type, old slice, cap int) slice {
//主要代码
newcap := old.cap
doublecap := newcap + newcap
//如果cap比两倍还大,直接将新容量等于cap
if cap > doublecap {
newcap = cap
} else {
//否则进行扩容
const threshold = 256
//如果旧容量小于256,直接进行翻倍
if old.cap < threshold {
newcap = doublecap
} else {
//否则进行一个for循环进行扩容
//扩容策略是
// 检查 0 < newcap 去预防 扩容溢出和无限的循环
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
//从两倍扩容变成一个1.25倍扩容然后再加192,这个函数给了两者一个平滑的过渡()
newcap += (newcap + 3*threshold) / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
//如果扩容会导致溢出,那直接将新容量等于cap
if newcap <= 0 {
newcap = cap
}
}
}
但是就这样结束了吗,我们将结果打印出来
1->2 2.000000倍
2->4 2.000000倍
4->8 2.000000倍
8->16 2.000000倍
16->32 2.000000倍
32->64 2.000000倍
64->128 2.000000倍
128->256 2.000000倍
256->512 2.000000倍
512->848 1.656250倍
848->1280 1.509434倍
1280->1792 1.400000倍
1792->2560 1.428571倍
2560->3408 1.331250倍
3408->5120 1.502347倍
5120->7168 1.400000倍
7168->9216 1.285714倍
9216->12288 1.333333倍
我们主要看256这个临界点的值
256*1.25+192=512 确实没什么问题
但是下一个就出现问题了
512*1.25+192=832
答案应该是832才对,但是扩容的结果确实848。
后来才发现扩容源码不仅这点,下面的操作也会对容量进行改变
//关键代码
//计算新的容量所需要的空间
newlenmem = uintptr(cap) * goarch.PtrSize
//关键这步,堆内存块进行对齐,
capmem = roundupsize(uintptr(newcap) * goarch.PtrSize)
///判断是否溢出
overflow = uintptr(newcap) > maxAlloc/goarch.PtrSize
//修改后,newcap变成了842
newcap = int(capmem / goarch.PtrSize)