Slice 原理
Slice数据结构和原理
1、相对于数组,Slice的长度是动态可变的。如下:
代码语言:javascript复制func CreatSlice() {
s := make([]int, len(), cap())
var s1 []int
}
func CreatArr() {
var a [length]int
}
可以很清楚的看到,数组的长度是在编译时静态计算的,并且数组无法在运行时动态扩缩容量的。
2、在 go 的 /src/runtime/slice.go
中可以看到,如下:
type slice struct {
array unsafe.Pointer
len int
cap int
}
此外无论是数组还是 Slice 都是按照值传递。
3、slice和数组的传递性能区别:
代码语言:javascript复制func CallSlice(s []int) {
}
func CallArr(s [10000]int) {
}
func BenchMarkCallSlice(b *testing.B) {
s := make([]int, 10000)
for i := 0; i < b.N; i {
CallSlice(s)
}
}
func BenchMarkCallArr(b *testing.B) {
var a [10000]int
for i := 0; i < b.N; i {
CallArr(a)
}
}
结果如下:
Slice的实践
1、slice扩容过程中的坑。
代码语言:javascript复制var s []int
fmt.Println(len(s),cap(s))//0,0
这个就很简单的可以理解了,增加一个元素,容量和长度增加1个
代码语言:javascript复制s = append(s, 0)
fmt.Println(len(s), cap(s)) //1,1
//s = append(s, 1, 2)
//fmt.Println(len(s), cap(s)) //3,3
s = append(s, 1)
fmt.Println(len(s), cap(s)) //2,2
s = append(s, 2)
fmt.Println(len(s), cap(s)) //3,4
但是这个注释掉的和下面的有什么区别?为什么长度一样,容量不一样?
因为切片的每次扩容是前面扩容过的一倍,注释掉的代码就是一下append了两个,所以容量也是增加两个(内存优化)。
但是我们连续两次的append,第一次append后的容量为2,是1的二倍,然后再次append,再次翻倍,这也就是为什么为4。
最后:
代码语言:javascript复制for i := 3; i < 1025; i {
s = append(s, i)
}
fmt.Println(len(s), cap(s)) //1025,1280
为什么容量会为1280,而不是2048呢?
talk is cheap, show code。
我们可以在 runtime/slice 中找到 growslice 的方法,如下:
代码语言:javascript复制newcap := old.cap
doublecap := newcap newcap
if cap > doublecap {
newcap = cap
} else {
if old.cap < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap = newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
之后我们就能看到当容量大于等于1024情况下,增长倍数近似看为1.25倍。
扩容陷阱
先看三个例子:
demo1
代码语言:javascript复制func main() {
var s []int
for i := 0; i < 3; i {
s = append(s, i)
}
modifySlice(s)
fmt.Println(s) //[1024,1,2]
}
func modifySlice(s []int) {
s[0] = 1024
}
demo2
代码语言:javascript复制func main() {
var s []int
for i := 0; i < 3; i {
s = append(s, i)
}
modifySlice(s)
fmt.Println(s) //[1024,1,2]
}
func modifySlice(s []int) {
s = append(s, 2048)
s[0] = 1024
}
demo3
代码语言:javascript复制func main() {
var s []int
for i := 0; i < 3; i {
s = append(s, i)
}
modifySlice(s)
fmt.Println(s) //[0 1 2]
}
func modifySlice(s []int) {
s = append(s, 2048)
s = append(s, 4096)
s[0] = 1024
}
demo4
代码语言:javascript复制func main() {
var s []int
for i := 0; i < 3; i {
s = append(s, i)
}
modifySlice(s)
fmt.Println(s) //[1024 1 2]
}
func modifySlice(s []int) {
s[0] = 1024
s = append(s, 2048)
s = append(s, 4096)
}
重点来说 demo2 和 demo3:
首先 demo2,在 modifySlice 的过程中,其实是传递的一个slice的一个结构体,
虽然共享了底层的存储,但是在modifySlice里的操作,比如扩容,在外层是看不到的,
所以也就是为什么2048无法打印出来。
但是s[0]是直接操作了原有的结构体,所以,s[0]是可以更改的。
接下来是 demo3,在第一次append的时候,len和cap都为4了,如果再次进行append会导致扩容,这个时候就导致两个s的底层存储空间发生变化。
所以无法进行更改了。
常见问题
如下:
代码语言:javascript复制 var s []int
b, _ := json.Marshal(s)
fmt.Println(string(b)) //null
代码语言:javascript复制 s := []int{}
b, _ := json.Marshal(s)
fmt.Println(string(b)) //[]
主要是和前端交互判断好是null还是[],否则容易产生bug。
以上文章来自 Carpe-Wang 同学的投稿。