原文连接: blog.thinkeridea.com/201901/go/s…html
array
和 slice
看似类似,却有着极大的不一样,但他们之间还有着千次万缕的联系 slice
是引用类型、是 array
的引用,至关于动态数组, 这些都是 slice
的特性,可是 slice
底层如何表现,内存中是如何分配的,特别是在程序中大量使用 slice
的状况下,怎样能够高效使用 slice
? 今天借助 Go
的 unsafe
包来探索 array
和 slice
的各类奥妙。linux
slice
是在 array
的基础上实现的,须要先详细了解一下数组。git
** 维基上如此介绍数组:**github
在计算机科学中,数组数据结构(英语:array data structure),简称数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储,利用元素的索引(index)能够计算出该元素对应的存储地址。golang
** 数组设计之初是在形式上依赖内存分配而成的,因此必须在使用前预先请求空间。这使得数组有如下特性:**算法
- 请求空间之后大小固定,不能再改变(数据溢出问题);
- 在内存中有空间连续性的表现,中间不会存在其余程序须要调用的数据,为此数组的专用内存空间;
- 在旧式编程语言中(若有中阶语言之称的C),程序不会对数组的操做作下界判断,也就有潜在的越界操做的风险(好比会把数据写在运行中程序须要调用的核心部分的内存上)。
根据维基的介绍,了解到数组是存储在一段连续的内存中,每一个元素的类型相同,便是每一个元素的宽度相同,能够根据元素的宽度计算元素存储的位置。shell
经过这段介绍总结一下数组有一下特性:编程
Go
中的数组如何实现的呢,偏偏就是这么实现的,实际上几乎全部计算机语言,数组的实现都是类似的,也拥有上面总结的特性。json
Go
语言的数组不一样于 C
语言或者其余语言的数组,C
语言的数组变量是指向数组第一个元素的指针;数组
而 Go
语言的数组是一个值,Go
语言中的数组是值类型,一个数组变量就表示着整个数组,意味着 Go
语言的数组在传递的时候,传递的是原数组的拷贝。
在程序中数组的初始化有两种方法 arr := [10]int{}
或 var arr [10]int
,可是不能使用 make
来建立,数组这节结束时再探讨一下这个问题。
使用 unsafe
来看一下在内存中都是如何存储的吧:
package main
import (
"fmt"
"unsafe"
)
func main() {
var arr = [3]int{1, 2, 3}
fmt.Println(unsafe.Sizeof(arr))
size := unsafe.Sizeof(arr[0])
// 获取数组指定索引元素的值
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size)))
// 设置数组指定索引元素的值
*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size)) = 10
fmt.Println(arr[1])
}
复制代码
这段代码的输出以下 (Go Playground):
12
2
10
复制代码
首先说 12
是 fmt.Println(unsafe.Sizeof(arr))
输出的,unsafe.Sizeof
用来计算当前变量的值在内存中的大小,12
这个表明一个 int
有4个字节,3 * 4
就是 12
。
这是在32位平台上运行得出的结果, 若是在64位平台上运行数组的大小是 24
。从这里能够看出 [3]int
在内存中由3个连续的 int
类型组成,且有 12
个字节那么长,这就说明了数组在内存中没有存储多余的数据,只存储元素自己。
size := unsafe.Sizeof(arr[0])
用来计算单个元素的宽度,int
在32位平台上就是4个字节,uintptr(unsafe.Pointer(&arr[0]))
用来计算数组起始位置的指针,1*size
用来获取索引为1的元素相对数组起始位置的偏移,unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size))
获取索引为1的元素指针,*(*int)
用来转换指针位置的数据类型, 由于 int
是4个字节,因此只会读取4个字节的数据,由元素类型限制数据宽度,来肯定元素的结束位置,所以获得的结果是 2
。
上一个步骤获取元素的值,其中先获取了元素的指针,赋值的时候只须要对这个指针位置设置值就能够了, *(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&arr[0])) + 1*size)) = 10
就是用来给指定下标元素赋值。
package main
import (
"fmt"
"unsafe"
)
func main() {
n:= 10
var arr = [n]int{}
fmt.Println(arr)
}
复制代码
如上代码,动态的给数组设定长度,会致使编译错误 non-constant array bound n
, 由此推导数组的全部操做都是编译时完成的,会转成对应的指令,经过这个特性知道数组的长度是数组类型不可或缺的一部分,而且必须在编写程序时肯定。
能够经过 GOOS=linux GOARCH=amd64 go tool compile -S array.go
来获取对应的汇编代码,在 array.go
中作一些数组相关的操做,查看转换对应的指令。
以前的疑问,为何数组不能用 make
建立? 上面分析了解到数组操做是在编译时转换成对应指令的,而 make
是在运行时处理(特殊状态下会作编译器优化,make能够被优化,下面 slice
分析时来说)。
由于数组是固定长度且是值传递,很不灵活,因此在 Go
程序中不多看到数组的影子。然而 slice
无处不在,slice
以数组为基础,提供强大的功能和遍历性。
slice
的类型规范是[]T,slice
T元素的类型。与数组类型不一样,slice
类型没有指定的长度。
** slice
申明的几种方法:**
`s := []int{1, 2, 3}` 简短的赋值语句
`var s []int` `var` 申明
`make([]int, 3, 8)` 或 `make([]int, 3)` `make` 内置方法建立
`s := ss[:5] ` 从切片或者数组建立
复制代码
** slice
有两个内置函数来获取其属性:**
`len` 获取 `slice` 的长度
`cap` 获取 `slice` 的容量
复制代码
slice
的属性,这东西是什么,还需借助 unsafe
来探究一下。
package main
import (
"fmt"
"unsafe"
)
func main() {
s := make([]int, 10, 20)
s[2] = 100
s[9] = 200
size := unsafe.Sizeof(0)
fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size*2)))
fmt.Println(*(*[20]int)(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(&s)))))
}
复制代码
这段代码的输出以下 (Go Playground):
c00007ce90
10
20
[0 0 100 0 0 0 0 0 0 200 0 0 0 0 0 0 0 0 0 0]
复制代码
这段输出除了第一个,剩余三个好像都能看出点什么, 10
不是建立 slice
的长度吗,20
不就是指定的容量吗, 最后这个看起来有点像 slice
里面的数据,可是数量貌似有点多,从第三个元素和第十个元素来看,正好是给 slice
索引 2
和 10
指定的值,可是切片不是长度是 10
个吗,难道这个是容量,容量恰好是 20
个。
第二和第三个输出很好弄明白,就是 slice
的长度和容量, 最后一个实际上是 slice
引用底层数组的数据,由于建立容量为 20
,因此底层数组的长度就是 20
,从这里了解到切片是引用底层数组上的一段数据,底层数组的长度就是 slice
的容量,因为数组长度不可变的特性,当 slice
的长度达到容量大小以后就须要考虑扩容,不是说数组长度不能变吗,那 slice
怎么实现扩容呢, 其实就是在内存上分配一个更大的数组,把当前数组上的内容拷贝到新的数组上, slice
来引用新的数组,这样就实现扩容了。
说了这么多,仍是没有看出来 slice
是如何引用数组的,额…… 以前的程序还有一个输出没有搞懂是什么,难道这个就是底层数组的引用。
package main
import (
"fmt"
"unsafe"
)
func main() {
arr := [10]int{1, 2, 3}
arr[7] = 100
arr[9] = 200
fmt.Println(arr)
s1 := arr[:]
s2 := arr[2:8]
size := unsafe.Sizeof(0)
fmt.Println("----------s1---------")
fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s1)))
fmt.Printf("%x\n", uintptr(unsafe.Pointer(&arr[0])))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size*2)))
fmt.Println(s1)
fmt.Println(*(*[10]int)(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(&s1)))))
fmt.Println("----------s2---------")
fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s2)))
fmt.Printf("%x\n", uintptr(unsafe.Pointer(&arr[0]))+size*2)
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s2)) + size)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s2)) + size*2)))
fmt.Println(s2)
fmt.Println(*(*[8]int)(unsafe.Pointer(*(*uintptr)(unsafe.Pointer(&s2)))))
}
复制代码
以上代码输出以下(Go Playground):
[1 2 3 0 0 0 0 100 0 200]
----------s1---------
c00001c0a0
c00001c0a0
10
10
[1 2 3 0 0 0 0 100 0 200]
[1 2 3 0 0 0 0 100 0 200]
----------s2---------
c00001c0b0
c00001c0b0
6
8
[3 0 0 0 0 100]
[3 0 0 0 0 100 0 200]
复制代码
这段输出看起来有点小复杂,第一行输出就不用说了吧,这个是打印整个数组的数据。先分析一下 s1
变量的下面的输出吧,s1 := arr[:]
引用了整个数组,因此在第五、6行输出都是10,由于数组长度为10,全部 s1
的长度和容量都为10,那第三、4行输出是什么呢,他们怎么都同样呢,以前分析数组的时候 经过 uintptr(unsafe.Pointer(&arr[0]))
来获取数组起始位置的指针的,那么第4行打印的就是数组的指针,这么就了解了第三行输出的是上面了吧,就是数组起始位置的指针,因此 *(*uintptr)(unsafe.Pointer(&s1))
获取的就是引用数组的指针,可是这个并非数组起始位置的指针,而是 slice
引用数组元素的指针,为何这么说呢?
接着看 s2
变量下面的输出吧,s2 := arr[2:8]
引用数组第3~8的元素,那么 s2
的长度就是 6。 根据经验能够知道 s2
变量输出下面第3行就是 slice
的长度,可是为啥第4行是 8
呢,slice
应用数组的指定索引发始位置到数组结尾就是 slice
的容量, 因此 因此从第3个位置到末尾,就是8个容量。在看第1行和第2行的输出,以前分析数组的时候经过 uintptr(unsafe.Pointer(&arr[0]))+size*2
来获取数组指定索引位置的指针,那么这段第2行就是数组索引为2的元素指针,*(*uintptr)(unsafe.Pointer(&s2))
是获取切片的指针,第1行和第2行输出一致,因此 slice
实际是引用数组元素位置的指针,并非数组起始位置的指针。
** 总结:**
slice
是的起始位置是引用数组元素位置的指针。slice
的长度是引用数组元素起始位置到结束位置的长度。slice
的容量是引用数组元素起始位置到数组末尾的长度。通过上面一轮分析了解到 slice
有三个属性,引用数组元素位置指针、长度和容量。实际上 slice
的结构像下图同样:
slice
是如何增加的,用 unsafe
分析一下看看:
package main
import (
"fmt"
"unsafe"
)
func main() {
s := make([]int, 9, 10)
// 引用底层的数组地址
fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s)))
s = append(s, 1)
// 引用底层的数组地址
fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s)))
s = append(s, 1)
// 引用底层的数组地址
fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s)))
}
复制代码
以上代码的输出(Go Playground):
c000082e90
9 10
c000082e90
10 10
c00009a000
11 20
复制代码
从结果上看前两次地址是同样的,初始化一个长度为9,容量为10的 slice
,当第一次 append
的时候容量是足够的,因此底层引用数组地址未发生变化,此时 slice
的长度和容量都为10,以后再次 append
的时候发现底层数组的地址不同了,由于 slice
的长度超过了容量,可是新的 slice
容量并非11而是20,这要说 slice
的机制了,由于数组长度不可变,想扩容 slice
就必须分配一个更大的数组,并把以前的数据拷贝到新数组,若是一次只增长1个长度,那就会那发生大量的内存分配和数据拷贝,这个成本是很大的,因此 slice
是有一个增加策略的。
Go
标准库 runtime/slice.go
当中有详细的 slice
增加策略的逻辑:
func growslice(et *_type, old slice, cap int) slice {
.....
// 计算新的容量,核心算法用来决定slice容量增加
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
if newcap <= 0 {
newcap = cap
}
}
}
// 根据et.size调整新的容量
var overflow bool
var lenmem, newlenmem, capmem uintptr
switch {
case et.size == 1:
lenmem = uintptr(old.len)
newlenmem = uintptr(cap)
capmem = roundupsize(uintptr(newcap))
overflow = uintptr(newcap) > maxAlloc
newcap = int(capmem)
case et.size == sys.PtrSize:
lenmem = uintptr(old.len) * sys.PtrSize
newlenmem = uintptr(cap) * sys.PtrSize
capmem = roundupsize(uintptr(newcap) * sys.PtrSize)
overflow = uintptr(newcap) > maxAlloc/sys.PtrSize
newcap = int(capmem / sys.PtrSize)
case isPowerOfTwo(et.size):
var shift uintptr
if sys.PtrSize == 8 {
// Mask shift for better code generation.
shift = uintptr(sys.Ctz64(uint64(et.size))) & 63
} else {
shift = uintptr(sys.Ctz32(uint32(et.size))) & 31
}
lenmem = uintptr(old.len) << shift
newlenmem = uintptr(cap) << shift
capmem = roundupsize(uintptr(newcap) << shift)
overflow = uintptr(newcap) > (maxAlloc >> shift)
newcap = int(capmem >> shift)
default:
lenmem = uintptr(old.len) * et.size
newlenmem = uintptr(cap) * et.size
capmem = roundupsize(uintptr(newcap) * et.size)
overflow = uintptr(newcap) > maxSliceCap(et.size)
newcap = int(capmem / et.size)
}
......
var p unsafe.Pointer
if et.kind&kindNoPointers != 0 {
p = mallocgc(capmem, nil, false) // 分配新的内存
memmove(p, old.array, lenmem) // 拷贝数据
memclrNoHeapPointers(add(p, newlenmem), capmem-newlenmem)
} else {
p = mallocgc(capmem, et, true) // 分配新的内存
if !writeBarrier.enabled {
memmove(p, old.array, lenmem)
} else {
for i := uintptr(0); i < lenmem; i += et.size {
typedmemmove(et, add(p, i), add(old.array, i)) // 拷贝数据
}
}
}
return slice{p, old.len, newcap} // 新slice引用新的数组,长度为旧数组的长度,容量为新数组的容量
}
复制代码
基本呢就三个步骤,计算新的容量、分配新的数组、拷贝数据到新数组,社区不少人分享 slice
的增加方法,实际都不是很精确,由于你们只分析了计算 newcap
的那一段,也就是上面注释的第一部分,下面的 switch
根据 et.size
来调整 newcap
一段被直接忽略,社区的结论是:"若是 selic
的容量小于1024个元素,那么扩容的时候 slice
的 cap
就翻番,乘以2;一旦元素个数超过1024个元素,增加因子就变成1.25,即每次增长原来容量的四分之一" 大多数状况也确实如此,可是根据 newcap
的计算规则,若是新的容量超过旧的容量2倍时会直接按新的容量分配,真的是这样吗?
package main
import (
"fmt"
)
func main() {
s := make([]int, 10, 10)
fmt.Println(len(s), cap(s))
s2 := make([]int, 40)
s = append(s, s2...)
fmt.Println(len(s), cap(s))
}
复制代码
以上代码的输出(Go Playground):
10 10
50 52
复制代码
这个结果有点出人意料, 若是是2倍增加应该是 10 * 2 * 2 * 2
结果应该是80, 若是说新的容量高于旧容量的两倍但结果也不是50,实际上 newcap
的结果就是50,那段逻辑很好理解,可是switch
根据 et.size
来调整 newcap
后就是52了,这段逻辑走到了 case et.size == sys.PtrSize
这段,详细的之后作源码分析再说。
** 总结 **
slice
的长度超过其容量,会分配新的数组,并把旧数组上的值拷贝到新的数组slice
并操过其容量, 若是 selic
的容量小于1024个元素,那么扩容的时候 slice
的 cap
就翻番,乘以2;一旦元素个数超过1024个元素,增加因子就变成1.25,即每次增长原来容量的四分之一。slice
发生扩容,引用新数组后,slice
操做不会再影响旧的数组,而是新的数组(社区常常讨论的传递 slice
容量超出后,修改数据不会做用到旧的数据上),因此每每设计函数若是会对长度调整都会返回新的 slice
,例如 append
方法。slice
不发生扩容,全部的修改都会做用在原数组上,那若是把 slice
传递给一个函数或者赋值给另外一个变量会发生什么呢,slice
是引用类型,会有新的内存被分配吗。
package main
import (
"fmt"
"strings"
"unsafe"
)
func main() {
s := make([]int, 10, 20)
size := unsafe.Sizeof(0)
fmt.Printf("%p\n", &s)
fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size*2)))
slice(s)
s1 := s
fmt.Printf("%p\n", &s1)
fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s1)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size*2)))
fmt.Println(strings.Repeat("-", 50))
*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size)) = 20
fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size*2)))
fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s1)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s1)) + size*2)))
fmt.Println(s)
fmt.Println(s1)
fmt.Println(strings.Repeat("-", 50))
s2 := s
s2 = append(s2, 1)
fmt.Println(len(s), cap(s), s)
fmt.Println(len(s1), cap(s1), s1)
fmt.Println(len(s2), cap(s2), s2)
}
func slice(s []int) {
size := unsafe.Sizeof(0)
fmt.Printf("%p\n", &s)
fmt.Printf("%x\n", *(*uintptr)(unsafe.Pointer(&s)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size)))
fmt.Println(*(*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s)) + size*2)))
}
复制代码
这个例子(Go Playground)比较长就不逐一分析了,在这个例子里面调用函数传递 slice
其变量的地址发生了变化, 可是引用数组的地址,slice
的长度和容量都没有变化, 这说明是对 slice
的浅拷贝,拷贝 slice
的三个属性建立一个新的变量,虽然引用底层数组仍是一个,可是变量并非一个。
第二个建立 s1
变量,使用 s
为其赋值,发现 s1
和函数调用同样也是 s
的浅拷贝,以后修改 s1
的长度发现 s1
的长度发生变化,可是 s
的长度保持不变, 这也说明 s1
就是 s
的浅拷贝。
这样设计有什么优点呢,第三步建立 s2
变量, 而且 append
一个元素, 发现 s2
的长度发生变化了, s
并无,虽然这个数据就在底层数组上,可是用常规的方法 s
是看不到第11个位置上的数据的, s1
由于长度覆盖到第11个元素,全部可以看到这个数据的变化。这里能看到采用浅拷贝的方式可使得切片的属性各自独立,而不会相互影响,这样能够有必定的隔离性,缺点也很明显,若是两个变量都引用同一个数组,同时 append
, 在不发生扩容的状况下,老是最后一个 append
的结果被保留,可能引发一些编程上疑惑。
** 总结 **
slice
是引用类型,可是和 C
传引用是有区别的, C
里面的传引用是在编译器对原变量数据引用, 并不会发生内存分配,而 Go
里面的引用类型传递和赋值会进行浅拷贝,在32位平台上有12个字节的内存分配, 在64位上有24字节的内存分配。
*** 传引用和引用类型是有区别的, slice
是引用类型。***
slice
有三种状态:零切片、空切片、nil切片。
全部的类型都有零值,若是 slice
所引用数组元素都没有赋值,就是全部元素都是类型零值,那这就是零切片。
package main
import "fmt"
func main() {
var s = make([]int, 10)
fmt.Println(s)
var s1 = make([]*int, 10)
fmt.Println(s1)
var s2 = make([]string, 10)
fmt.Println(s2)
}
复制代码
以上代码输出(Go Playground):
[0 0 0 0 0 0 0 0 0 0]
[<nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil> <nil>]
[ ]
复制代码
零切片很好理解,数组元素都为类型零值即为零切片,这种状态下的 slice
和正常的 slice
操做没有任何区别。
空切片能够理解就是切片的长度为0,就是说 slice
没有元素。 社区大多数解释空切片为引用底层数组为 zerobase
这个特殊的指针。可是从操做上看空切片全部的表现就是切片长度为0,若是容量也为零底层数组就会指向 zerobase
,这样就不会发生内存分配, 若是容量不会零就会指向底层数据,会有内存分配。
package main
import (
"fmt"
"reflect"
"strings"
"unsafe"
)
func main() {
var s []int
s1 := make([]int, 0)
s2 := make([]int, 0, 0)
s3 := make([]int, 0, 100)
arr := [10]int{}
s4 := arr[:0]
fmt.Println(strings.Repeat("--s--", 10))
fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s)))
fmt.Println(s)
fmt.Println(s == nil)
fmt.Println(strings.Repeat("--s1--", 10))
fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s1)))
fmt.Println(s1)
fmt.Println(s1 == nil)
fmt.Println(strings.Repeat("--s2--", 10))
fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s2)))
fmt.Println(s2)
fmt.Println(s2 == nil)
fmt.Println(strings.Repeat("--s3--", 10))
fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s3)))
fmt.Println(s3)
fmt.Println(s3 == nil)
fmt.Println(strings.Repeat("--s4--", 10))
fmt.Println(*(*reflect.SliceHeader)(unsafe.Pointer(&s4)))
fmt.Println(s4)
fmt.Println(s4 == nil)
}
复制代码
以上代码输出(Go Playground):
--s----s----s----s----s----s----s----s----s----s--
{0 0 0}
[]
--s1----s1----s1----s1----s1----s1----s1----s1----s1----s1--
{18349960 0 0}
[]
--s2----s2----s2----s2----s2----s2----s2----s2----s2----s2--
{18349960 0 0}
[]
--s3----s3----s3----s3----s3----s3----s3----s3----s3----s3--
{824634269696 0 100}
[]
--s4----s4----s4----s4----s4----s4----s4----s4----s4----s4--
{824633835680 0 10}
[]
复制代码
以上示例中除了 s
其它的 slice
都是空切片,打印出来所有都是 []
,s
是nil切片下一小节说。要注意 s1
和 s2
的长度和容量都为0,且引用数组指针都是 18349960
, 这点过重要了,由于他们都指向 zerobase
这个特殊的指针,是没有内存分配的。
什么是nil切片,这个名字说明nil切片没有引用任何底层数组,底层数组的地址为nil就是nil切片。上一小节中的 s
就是一个nil切片,它的底层数组指针为0,表明是一个 nil
指针。
零切片就是其元素值都是元素类型的零值的切片。
空切片就是数组指针不为nil,且 slice
的长度为0。
nil切片就是引用底层数组指针为 nil
的 slice
。
操做上零切片、空切片和正常的切片都没有任何区别,可是nil切片会多两个特性,一个nil切片等于 nil
值,且进行 json
序列化时其值为 null
,nil切片还能够经过赋值为 nil
得到。
对数组和 slice
作了性能测试,源码在 GitHub。
对不一样容量和数组和切片作性能测试,代码以下,分为:100、1000、10000、100000、1000000、10000000
func BenchmarkSlice100(b *testing.B) {
for i := 0; i < b.N; i++ {
s := make([]int, 100)
for i, v := range s {
s[i] = 1 + i
_ = v
}
}
}
func BenchmarkArray100(b *testing.B) {
for i := 0; i < b.N; i++ {
a := [100]int{}
for i, v := range a {
a[i] = 1 + i
_ = v
}
}
}
复制代码
测试结果以下:
goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/example/array_slice/test
BenchmarkSlice100-8 20000000 69.8 ns/op 0 B/op 0 allocs/op
BenchmarkArray100-8 20000000 69.0 ns/op 0 B/op 0 allocs/op
BenchmarkSlice1000-8 5000000 318 ns/op 0 B/op 0 allocs/op
BenchmarkArray1000-8 5000000 316 ns/op 0 B/op 0 allocs/op
BenchmarkSlice10000-8 200000 9024 ns/op 81920 B/op 1 allocs/op
BenchmarkArray10000-8 500000 3143 ns/op 0 B/op 0 allocs/op
BenchmarkSlice100000-8 10000 114398 ns/op 802816 B/op 1 allocs/op
BenchmarkArray100000-8 20000 61856 ns/op 0 B/op 0 allocs/op
BenchmarkSlice1000000-8 2000 927946 ns/op 8003584 B/op 1 allocs/op
BenchmarkArray1000000-8 5000 342442 ns/op 0 B/op 0 allocs/op
BenchmarkSlice10000000-8 100 10555770 ns/op 80003072 B/op 1 allocs/op
BenchmarkArray10000000-8 50 22918998 ns/op 80003072 B/op 1 allocs/op
PASS
ok github.com/thinkeridea/example/array_slice/test 23.333s
复制代码
从上面的结果能够发现数组和 slice
在1000之内的容量上时性能机会一致,并且都没有内存分配,这应该是编译器对 slice
的特殊优化。
从10000~1000000容量时数组的效率就比slice
好了一倍有余,主要缘由是数组在没有内存分配作了编译优化,而 slice
有内存分配。
可是10000000容量日后数组性能大幅度降低,slice
是数组性能的两倍,两个都在运行时作了内存分配,其实这么大的数组还真是不常见,也没有比较作编译器优化了。
slice
和数组有些差异,特别是应用层上,特性差异很大,那什么时间使用数组,什么时间使用切片呢。
以前作了性能测试,在1000之内性能几乎一致,只有10000~1000000时才会出现数组性能好于 slice
,因为数组在编译时肯定长度,也就是再编写程序时必须确认长度,全部往常不会用到更大的数组,大多数都在1000之内的长度。我认为若是在编写程序是就已经肯定数据长度,建议用数组,并且竟多是局部使用的位置建议用数组(避免传递产生值拷贝),好比一天24小时,一小时60分钟,ip是4个 byte
这种状况是能够用时数组的。
为何推荐用数组,只要能在编写程序是肯定数据长度我都会用数组,由于其类型会帮助阅读理解程序,dayHour := [24]Data
一眼就知道是按小时切分数据存储的,如要传递数组时能够考虑传递数组的指针,固然会带来一些操做不方便,往常我使用数组都是不须要传递给其它函数的,可能会在 struct
里面保存数组,而后传递 struct
的指针,或者用 unsafe
来反解析数组指针到新的数组,也不会产生数据拷贝,而且只增长一句转换语句。slice
会比数组多存储三个 int
的属性,并且指针引用会增长 GC
扫描的成本,每次传递都会对这三个属性进行拷贝,若是能够也能够考虑传递 slice
的指针,指针只有一个 int
的大小。
** 对于不肯定大小的数据只能用 slice
,不然就要本身作扩容很麻烦, 对于肯定大小的集合建议使用数组。**
转载:
本文做者: 戚银(thinkeridea)
本文连接: blog.thinkeridea.com/201901/go/s…
版权声明: 本博客全部文章除特别声明外,均采用 CC BY 4.0 CN协议 许可协议。转载请注明出处!