Go面试必考题目之slice篇

栏目: Go · 发布时间: 5年前

内容简介:下面代码中,会输出什么?上面的这几道题,也是Go编程中比较容易让人感到迷惑的地方,但如果懂slice的底层原理,你就能避开这些坑且能轻松的答对上面几道题。array底层

下面代码中,会输出什么?

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)
}

上面的这几道题,也是 Go 编程中比较容易让人感到迷惑的地方,但如果懂slice的底层原理,你就能避开这些坑且能轻松的答对上面几道题。

array底层

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]
}

因为是值拷贝的原因, c 的修改并没有影响到 b

slice底层

掌握Go的slice,底层结构必须要了解。

type slice struct {
	array unsafe.Pointer
	len   int
	cap   int
}

Go面试必考题目之slice篇

slice的底层结构由一个指向数组的指针 ptr 和长度 len ,容量 cap 构成,也就是说slice的数据存在数组当中。

slice的重要知识点

1. slice的底层是数组指针。

2. 当 append 后,slice长度不超过容量 cap ,新增的元素将直接加在数组中。

3. 当 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
}

因为slice的底层是数组指针,所以slice  a s 指向的是同一个底层数组,所以当修改 s[0] 时, a 也会被修改。

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
}

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)) // 输出:[1, 2, 3] 4 6
    fmt.Println(a,len(s),cap(s)) // 输出:[1, 2, 3] 3 3
}

上面代码中,当对 s 进行 append 后,它的长度和容量都发生了变化,最重要的是它的底层数组指针指向了一个新的数组,然后将旧数组的值复制到了新的数组当中。

a 没有被影响是因为进行 s[0] = 999 赋值,是因为 s 的底层数组指针已经指向了一个新的数组。

我们通过观察 容量 cap 的变化,可以知道slice的底层数组是否发生了变化。 cap 的增长算法并不是每次都将容量扩大一倍的,感兴趣的读者可以看下slice的扩容算法。

使用array还是slice?

一个很重要的知识点是: Go的函数传参,都是以值的形式传参。 而且Go是没有引用的,可以看下这篇文章

如果要给函数传递一个有100w个元素的array时,直接使用array传递的效率是非常低的,因为array是值拷贝,100w个元素都复制一遍是非常可怕的;这时就应该使用slice作为参数,就相当于传递了一个指针。

如果元素数量比较少,使用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]
    // 数组赋值是值拷贝,所以不会影响原来的数组
}

总结

1. 谨记slice的底层结构是指针数组,并且 len cap 是值类型。

2. 使用 cap 观察append后是否分配了新的数组。

3. Go的函数传参都是值拷贝传递。

感谢阅读,欢迎大家指正,留言交流~

Go面试必考题目之slice篇


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

另一个地球

另一个地球

[美]马克·格雷厄姆、威廉·H·达顿 / 胡泳、徐嫩羽 / 电子工业出版社 / 2015-10-1 / 78

互联网在日常工作和生活中扮演日益重要的角色,互联网将如何重塑社会?本书通过汇集有关互联网文化、经济、政治角色等问题的研究成果,提供了特定社会制度背景下解决这一问题的根本办法。 关于互联网的研究是蓬勃发展的崭新领域,牛津大学互联网研究院(OII)作为创新型的跨学科学院,自成立起就专注于互联网研究。牛津大学互联网研究院关于互联网+社会的系列讲座在一定程度上塑造了互联网+社会。本书内容基于不同学科......一起来看看 《另一个地球》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

html转js在线工具
html转js在线工具

html转js在线工具