前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >面试官:讲下Go语言中slice的原理和坑

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

作者头像
小锟哥哥
发布2022-08-31 11:41:20
2960
发布2022-08-31 11:41:20
举报
文章被收录于专栏:GoLang全栈

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 同学的投稿。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-06-09,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 GoLang全栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Slice 原理
    • Slice数据结构和原理
    • Slice的实践
    • 扩容陷阱
    • 常见问题
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档