Go面试必考题目之slice篇

deletelazy · · 809 次点击 · 开始浏览    置顶
**来源:** https://mp.weixin.qq.com/s/gUkYwCodYI0iAD5LMYXdMA **欢迎关注公众号《Go后端干货》** **各种Go,后端技术,面试题分享** 下面代码中,会输出什么? ``` func Assign1(s []int) { s = []int{6, 6, 6} } func Reverse0(s [5]int) { for i, j := 0, len(s)-1; i < j; i++ { j = len(s) - (i + 1) s[i], s[j] = s[j], s[i] } } func Reverse1(s []int) { for i, j := 0, len(s)-1; i < j; i++ { j = len(s) - (i + 1) s[i], s[j] = s[j], s[i] } } func Reverse2(s []int) { s = append(s, 999) for i, j := 0, len(s)-1; i < j; i++ { j = len(s) - (i + 1) s[i], s[j] = s[j], s[i] } } func Reverse3(s []int) { s = append(s, 999, 1000, 1001) for i, j := 0, len(s)-1; i < j; i++ { j = len(s) - (i + 1) s[i], s[j] = s[j], s[i] } } func main() { s := []int{1, 2, 3, 4, 5, 6} Assign1(s) fmt.Println(s) // (1) array := [5]int{1, 2, 3, 4, 5} Reverse0(array) fmt.Println(array) // (2) s = []int{1, 2, 3} Reverse2(s) fmt.Println(s) // (3) var a []int for i := 1; i <= 3; i++ { a = append(a, i) } Reverse2(a) fmt.Println(a) // (4) var b []int for i := 1; i <= 3; i++ { b = append(b, i) } Reverse3(b) fmt.Println(b) // (5) c := [3]int{1, 2, 3} d := c c[0] = 999 fmt.Println(d) // (6) } ``` &emsp;&emsp;上面的这几道题,也是Go编程中比较容易让人感到迷惑的地方,但如果懂slice的底层原理,你就能避开这些坑且能轻松的答对上面几道题。 ### array底层 &emsp;&emsp;Go的数组array底层和C的数组一样,是一段连续的内存空间,通过下标访问数组中的元素。array只有长度`len`属性而且是固定长度的。 array的赋值是值拷贝的,看以下代码: ``` func main() { c := [3]int{1, 2, 3} d := c c[0] = 999 fmt.Println(d) // 输出[1, 2, 3] } ``` &emsp;&emsp;因为是值拷贝的原因,`c`的修改并没有影响到`d`。 ### slice底层 &emsp;&emsp;掌握Go的slice,底层结构必须要了解。 ``` type slice struct { array unsafe.Pointer len int cap int } ``` ![](https://s2.ax1x.com/2019/06/03/VYKb1x.png) &emsp;&emsp;slice的底层结构由一个指向数组的指针ptr和长度len,容量cap构成,也就是说slice的数据存在数组当中。 #### slice的重要知识点 - slice的底层是数组指针。 - 当append后,slice长度不超过容量cap,新增的元素将直接加在数组中。 - 当append后,slice长度超过容量cap,将会返回一个新的slice。 关于知识点1,看以下代码: ``` func main() { s := []int{1, 2, 3} // len=3, cap=3 a := s s[0] = 888 s = append(s, 4) fmt.Println(a, len(a), cap(a)) // 输出:[888 2 3] 3 3 fmt.Println(s, len(s), cap(s)) // 输出:[888 2 3 4] 4 6 } ``` &emsp;&emsp;因为slice的底层是数组指针,所以slice `a`和`s`指向的是同一个底层数组,所以当修改`s[0]`时,`a`也会被修改。 &emsp;&emsp;当`s`进行`append`时,因为长度`len`和容量`cap`是int值类型,所以不会影响到`a`。 关于知识点2,看以下代码: ``` func main() { s := make([]int, 0, 4) s = append(s, 1, 2, 3) fmt.Println(s, len(s), cap(s)) // 输出:[1, 2, 3] 3 4 s = append(s, 4) fmt.Println(s, len(s), cap(s)) // 输出:[1, 2, 3, 4] 4 4 } ``` &emsp;&emsp;当`s`进行`append`后,长度没有超过容量,所以底层数组的指向并没有发生变化,只是将值添加到数组中。 关于知识点3,看以下代码: ``` func main() { s := []int{1, 2, 3} fmt.Println(s, len(s), cap(s)) // 输出:[1, 2, 3] 3 3 a := s s = append(s, 4) // 超过了原来数组的容量 s[0] = 999 fmt.Println(s, len(s), cap(s)) // 输出:[999, 2, 3, 4] 4 6 fmt.Println(a,len(s),cap(s)) // 输出:[1, 2, 3] 3 3 } ``` &emsp;&emsp;上面代码中,当对`s`进行`append`后,它的长度和容量都发生了变化,最重要的是它的底层数组指针指向了一个新的数组,然后将旧数组的值复制到了新的数组当中。 &emsp;&emsp;`a`没有被影响是因为进行`s[0] = 999`赋值,是因为`s`的底层数组指针已经指向了一个新的数组。 &emsp;&emsp;我们通过观察容量`cap`的变化,可以知道slice的底层数组是否发生了变化。`cap`的增长算法并不是每次都将容量扩大一倍的,感兴趣的读者可以看下slice的扩容算法。 ### 使用array还是slice? &emsp;&emsp;一个很重要的知识点是:Go的函数传参,都是以值的形式传参。而且Go是没有引用的,可以看下这篇文章。 &emsp;&emsp;如果要给函数传递一个有100w个元素的array时,直接使用array传递的效率是非常低的,因为array是值拷贝,100w个元素都复制一遍是非常可怕的;这时就应该使用slice作为参数,就相当于传递了一个指针。 &emsp;&emsp;如果元素数量比较少,使用array还是slice作为参数,效率差别并不大。 ### 题目解析 ``` package main import "fmt" func Assign1(s []int) { s = []int{6, 6, 6} } func Reverse0(s [5]int) { for i, j := 0, len(s)-1; i < j; i++ { j = len(s) - (i + 1) s[i], s[j] = s[j], s[i] } } func Reverse1(s []int) { for i, j := 0, len(s)-1; i < j; i++ { j = len(s) - (i + 1) s[i], s[j] = s[j], s[i] } } func Reverse2(s []int) { s = append(s, 999) for i, j := 0, len(s)-1; i < j; i++ { j = len(s) - (i + 1) s[i], s[j] = s[j], s[i] } } func Reverse3(s []int) { s = append(s, 999, 1000, 1001) for i, j := 0, len(s)-1; i < j; i++ { j = len(s) - (i + 1) s[i], s[j] = s[j], s[i] } } func main() { s := []int{1, 2, 3, 4, 5, 6} Assign1(s) fmt.Println(s) // (1) 输出[1, 2, 3, 4, 5, 6] // 因为是值拷贝传递,Assign1里的s和main里的s是不同的两个指针 array := [5]int{1, 2, 3, 4, 5} Reverse0(array) fmt.Println(array) // (2) 输出[1, 2, 3, 4, 5] // 传递时对array进行了一次值拷贝,不会影响原来的array s = []int{1, 2, 3} Reverse2(s) fmt.Println(s) // (3) 输出[1, 2, 3] // 在没有对s进行append时,len(s)=3,cap(s)=3 // append之后超过了容量,返回了一个新的slice // 相当于只改变了新的slice,旧的slice没影响 var a []int for i := 1; i <= 3; i++ { a = append(a, i) } Reverse2(a) fmt.Println(a) // (4) 输出[999, 3, 2] // 在没有对a进行append时,len(a)=3,cap(a)=4 // append后没有超过容量,所以元素直接加在了数组上 // 虽然函数Reverse2里将a的len加1了,但它只是一个值拷贝 // 不会影响main里的a,所以main里的len(a)=3 var b []int for i := 1; i <= 3; i++ { b = append(b, i) } Reverse3(b) fmt.Println(b) // (5) 输出[1, 2, 3] // 原理同(3) c := [3]int{1, 2, 3} d := c c[0] = 999 fmt.Println(d) // (6) 输出[1, 2, 3] // 数组赋值是值拷贝,所以不会影响原来的数组 } ``` ### 总结 - 谨记slice的底层结构是指针数组,并且`len`和`cap`是值类型。 - 使用`cap`观察`append`后是否分配了新的数组。 - Go的函数传参都是值拷贝传递。 #### 感谢阅读,欢迎大家指正,留言交流~ ![](https://s2.ax1x.com/2019/06/03/VYKXnO.md.jpg)

入群交流(该群和以上内容无关):Go中文网 QQ交流群:731990104 或 加微信入微信群:274768166 备注:入群; 公众号:Go语言中文网

809 次点击  
加入收藏 微博
6 回复  |  直到 2019-06-04 18:42:20
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传