Go-数组与slice

本文将讲解Go语言中的数组与slice。之前看到网上好多 《深入理解slice》、《深入解析slice》... 的文章,我是比较佩服的,他们从应用、源码、汇编代码等各个角度分析了slice与数组,感叹他们已经领先自己好多了,于是把他们的文章结合源码自己学习了一遍。 佩服之余,也在想我有必要再写一篇吗?本着记录自己学习成果、便于新手入门的原则,我还是自己写一篇。尽量不写汇编,免得吓跑大家~
本文不但会讲解slice的结构、存储,还会将slice常见的api分析一下。

1 数组

数组是一块连续的内存。其定义了类型跟长度。因为其长度固定,不易扩展,所以在Go语言中,我们很少直接使用数组。个人理解,数组更多地是作为slice的底层存储来使用。

2 slice

slice可以理解为其他语言中的动态数组。

2.1 定义与数据结构

slice 结构是这样的:

type slice struct {
    array unsafe.Pointer  // 看就是这个数组
    len   int // 元素长度
    cap   int // 容量
}

其实定义中的array 就是一个数组,这个数组可以被多个slice共享。多个slice共享数组可能会引起一些问题,我们在下面会讲。
slice的定义有如下几种方式:

// 方式一 make
slice1 := make([]int, 3, 3)
// 方式二 空slice
var slice2 []int
// 方式三 利用array定义
arr := [3]int{1,2,3}
slice3 := arr[0:3]

无论哪种,其底层使用的函数 都是 makeslice

插播一条,如果找某一句go代码是对应源码中的什么函数?我一般先编译,然后反汇编。

// 编译
go build --gcflags "-N -l" -o main4 main.go
// 反汇编
go tool objdump -s "main\.main" main4

makeslice 主要做了两件事情:
(1)根据类型跟cap 计算内存容量,分配内存;
(2)利用上面分配的内存、len、cap 创建slice。

2.2 append

2.2.1 append之后,新生成的slice跟原来的slice不是一个了

slice最常见的操作就是append。append是向一个slice插入某个元素。执行完append之后会生成一个新的slice。也就是说append之后的slice跟之前的slice肯定不是一个
看个例子:

func TestSliceAppend_sliceAddrChange(t *testing.T)  {
    arr := [3]int{1,2,3}
    slice := arr[0:2]
    newSlice := append(slice, 50)
    fmt.Printf("arrPoint = %p, slicePointer = %p, newSlicePoint = %p \n", &arr, &slice, &newSlice)
    fmt.Println("=============================================")
}

结果是

// 利用array新生成的slice, 以及利用slice append操作生成的newSlice 地址都是不一样的。
arrPoint = 0xc42006caa0, slicePointer = 0xc42006cac0, newSlicePoint = 0xc42006cae0 
2.2.2 append之后,底层数组变了吗?

那slice底层的数组会变吗?先看下面的例子

func TestSliceAppend_ArrayAddr(t *testing.T)  {
    arr := [3]int{1,2,3}
    slice := arr[0:2]
    newSlice := append(slice, 50)
    fmt.Printf("arrPoint = %p, slicePointer = %p, newSlicePoint = %p \n", &arr, &slice, &newSlice)
    fmt.Printf("arrPoint = %p, slice array Pointer = %p, newSlice array Point = %p \n", &arr[0], &slice[0], &newSlice[0])
    fmt.Println("=============================================")
}

结果是

arrPoint = 0xc42000ac00, slicePointer = 0xc42000ac20, newSlicePoint = 0xc42000ac40 
arrPoint = 0xc42000ac00, slice array Pointer = 0xc42000ac00, newSlice array Point = 0xc42000ac00

我们看到,底层数组的地址是一样的。说明两个slice 共用了底层的array。
这是一定的吗?再看个例子:

func TestSliceAppend_newArray(t *testing.T)  {
    fmt.Println("array 容量还够,则复用")
    arr := [3]int{1,2,3}
    slice := arr[0:3]
    newSlice := append(slice, 50)
    fmt.Printf("arrPoint = %p, slicePointer = %p, newSlicePoint = %p \n", &arr[0], &slice[0], &newSlice[0])
    fmt.Println("=============================================")
}

结论是这样的

arrPoint = 0xc42000ac00, slice array Pointer = 0xc42000ac00, newSlice array Point = 0xc42000ac00 

我们发现第一个slice的首元素地址跟数组首元素地址相同,但是newSlice就不同了。为什么呢?
这是因为原来底层数组的容量已经不够了,这是会新分配一段新内存,这样就跟原来的内存不一样的地址了

2.2.3 append如果底层数组的扩容,一般会扩容多大?

其实反汇编2.2.2 中发生底层数组扩容的代码,会发现发生扩容时调用的底层函数是growslice。
那么我们就直接总结一下结论:(如果对源码感兴趣,可以看一下 go/src/runtime/slice.go#L76 growslice 的定义)

当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。[里面会涉及到内存对齐的问题,如果涉及内存对齐,就不会这些倍数了]

2.3 slice作为参数

本小结主要是讲一点,slice不管是以其自身还是以其指针作为参数 传递给函数,都会进行值拷贝。所以我们建议使用指针。为什么呢?节省内存啊。如果一个slice大小1G,如果用其自身传递,那么就得拷贝1G;而用指针则只需要拷贝一个指针就OK了。

2.3.1 使用slice自身作为参数

使用slice自身作为参数的例子:

func TestParamValueOrPoint(t *testing.T)  {
    arrayA := [2]int{100, 200}
    sliceA := arrayA[:]
    fmt.Printf("sliceA : %p , %v\n", &sliceA, sliceA)
    testSlice(sliceA)
    fmt.Println("---------------------------")
}

func testSlice(x []int) {
    fmt.Printf("func Slice : %p , %v\n", &x, x)
}

结果是:

sliceA : 0xc42000abc0 , [100 200]
func Slice : 0xc42000ac00 , [100 200]

我们发现进入testSlice函数之后获取的参数地址,已经不是之前的slice地址了。

2.3.2 使用slice的指针作为参数

例子:

func testSlicePoint(x *[]int) {
    fmt.Printf("func Slice : %p , %v\n", x, *x)
    (*x)[1] += 100
}

func testArrayPoint(x *[2]int) {
    fmt.Printf("func Array : %p , %v\n", x, *x)
    (*x)[1] += 100
}

func TestParamUsePointer(t *testing.T)  {
    fmt.Println("验证 Go 中 数组跟slice都是用其指针作为参数传递")
    arrayA := [2]int{100, 200}
    testArrayPoint(&arrayA)   // 1.传数组指针
    sliceB := arrayA[:]
    testSlicePoint(&sliceB)   // 2.传切片
    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
    fmt.Printf("sliceB : %p , %v\n", &sliceB, sliceB)
}

结果:

func Array : 0xc420016f80 , [100 200]
func Slice : 0xc42000ac40 , [100 300]
arrayA : 0xc420016f80 , [100 400]
sliceB : 0xc42000ac40 , [100 400]

我们发现,此时是拷贝的地址,所以可以继续使用原来的slice。

2.4 for range

单独列出这部分来,想表达的是如果采用for range ,会对原slice的值进行复制,这样改变复制之后的值,对原slice是不会产生影响的。
举个例子:

func TestRange_noEffect(t *testing.T) {
    b := []int{1, 2}
    // 场景一  这个例子是为了测试for range. 说明for range 每次会将slice的内容copy给v, 所以v变化,也不会影响b
    fmt.Println("场景一 无影响")
    for _, v := range b {
        v++
        //fmt.Println(k, v)
    }
    for k, v := range b {
        fmt.Println(k, v)
    }
    fmt.Println("=============================")
}

结果是

场景一 无影响
0 1
1 2

如果想产生影响呢?

func TestRange_haveEffect(t *testing.T) {
    b := []int{1, 2}
    // 场景二 要想产生实际影响,要这样做
    fmt.Println("场景二 产生影响")
    for k, v := range b {
        b[k] = v + 1
    }
    for k, v := range b {
        fmt.Println(k, v)
    }
    fmt.Println("=============================")
}

结果会是

场景二 产生影响
0 2
1 3

2.5 copy

copy的作用是将源slice copy给目的slice,类型必须一致。其底层只是内存的copy。
对应的函数是 slicecopy 。
来个例子吧,也没啥好讲的

func TestSliceCopy(t *testing.T)  {
    fmt.Println("slicecopy 方法最终的复制结果取决于较短的那个切片,当较短的切片复制完成,整个复制过程就全部完成了。")
    array := []int{10, 20, 30, 40}
    slice := make([]int, 6)
    n := copy(slice, array)
    fmt.Println(n,slice)
    fmt.Println("=============================================")

    fmt.Println("即使array2有7个元素,但是slice2只接收了6个。")
    array2 := []int{10, 20, 30, 40, 50,60,70}
    slice2 := make([]int, 6)
    n2 := copy(slice2, array2)
    fmt.Println(n2,slice2)
    fmt.Println("=============================================")
}

3 总结

本文将自己在学习Go 数组跟slice的一些好文章进行了总结。从slice的定义、初始化、常用api这几块分析了一下,并举了一些典型的例子。有些地方提到了一点 go源码的函数。希望这篇文章对你有所帮助~

4 参考文献

golang中的slice https://www.jianshu.com/p/3273e9e32951
golang中的数组 https://www.jianshu.com/p/863ffd730cef
深度解密Go语言之Slice [https://mp.weixin.qq.com/s/MTZ0C9zYsNrb8wyIm2D8BA]
深入解析 Go 中 Slice 底层实现 (https://mp.weixin.qq.com/s/MTZ0C9zYsNrb8wyIm2D8BA)
https://halfrost.com/go_slice/
深入理解 Go Slice https://book.eddycjy.com/golang/slice/slice.html
数组和切片 https://draveness.me/golang/datastructure/golang-array-and-slice.html
go源码

5 其他

本文是《循序渐进go语言》的第八篇-《Go-数组与slice》。
如果有疑问,可以直接留言,也可以关注公众号 “链人成长chainerup” 提问留言,或者加入知识星球“链人成长” 与我深度链接~

你可能感兴趣的:(Go-数组与slice)