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篇


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

查看所有标签

猜你喜欢:

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

Responsive Web Design

Responsive Web Design

Ethan Marcotte / Happy Cog / 2011-6 / USD 18.00

From mobile browsers to netbooks and tablets, users are visiting your sites from an increasing array of devices and browsers. Are your designs ready? Learn how to think beyond the desktop and craft be......一起来看看 《Responsive Web Design》 这本书的介绍吧!

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具

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

html转js在线工具

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具