面试官:讲下Go语言中slice的原理和坑

2022-08-31 11:41:20 浏览数 (1)

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 中可以看到,如下:

代码语言:javascript复制
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 同学的投稿。

0 人点赞