Go切片详解

100 阅读10分钟

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 []intif 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

需要注意的是从数组切片的方法中可以有三个参数,详细如下(来自地鼠文档)

img

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.截取

img

三.slice切片的底层

对应问题

切片底层(跟谁学,好未来,伴鱼,知乎,七牛+3,京东,哔哩哔哩,腾讯+1,小米,字节,Aibee,网易)

slice,len,cap,共享,扩容(腾讯)

append底层 (七牛)

slice内存泄漏分析 (知乎)

1.切片的数据结构

img

由图可以看到切片的底层实际上是一个数据结构,包含指向数组的指针,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.0000002->4 2.0000004->8 2.0000008->16 2.00000016->32 2.00000032->64 2.00000064->128 2.000000128->256 2.000000256->512 2.000000512->848 1.656250848->1280 1.5094341280->1792 1.4000001792->2560 1.4285712560->3408 1.3312503408->5120 1.5023475120->7168 1.4000007168->9216 1.2857149216->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)