GO经典面试真题详解

本文所有内容都是来自于面试中被问到频率较多的问题,尤其是 slice 部分,就是面试官口中的:“你在使用切片时遇到过哪些坑呢?”,所以特整理这些内容,希望对大家工作和面试都能有帮助

Slice

slice 真正存储数据的地方,是一个数组。slice 的结构中存储的是指向所引用的数组指针地址。

同一数组多次append

func main() {
    x := make([]int, 0, 10)
    x = append(x, 1, 2, 3)
    y := append(x, 4)
    z := append(x, 5)

    fmt.Println(x, len(x), cap(x)) 
    fmt.Println(y, len(y), cap(y)) 
    fmt.Println(z, len(z), cap(z))
}

那么这段代码输出多少呢?

[1 2 3] 3 10
[1 2 3 5] 4 10
[1 2 3 5] 4 10

解析:

在使用 append 向 slice 追加元素时,若 slice 空间不足则会发生扩容,扩容会重新分配一块更大的内存。若不发生扩容则操作依然是同一个底层数组。

疑问点❓

若操作的是同一个底层数组,那x为什么未被改变?

x = append(x, 1, 2, 3),此时索引位0 值为1,索引位1 值为2,索引位2 值为3

y := append(x, 4),y基于x append,此时索引位3 值为4

z := append(x, 5),z基于x append,此时索引位3 值被覆盖成了5

打印数据时将从底层数组中找到对应长度的数据

由于x的长度为3,所以打印结果为1 2 3

y、z长度为4,但由于z append时将索引位3的值覆盖成了5,所以打印y、z结果为1 2 3 5


函数中的扩容

func append1(nums []int, a int) {
    nums = append(nums, a)
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    append1(nums, 6)

    fmt.Println(nums)
}

那么这段代码输出多少呢?

[1 2 3 4 5]

解析:

当切片存储容量不足时,则会把当前切片进行扩容并产生新的切片。新的切片具有新的地址,但是go是按值传递,所以新的切片地址只是局部变量,是不能够随着函数返回的。

若切片容量足够,切片作为函数传参扩容后返回的还是原来的切片内容。因为决定切片的还有len


闭包函数扩容

var f = func(s []int) {
    s = append(s, 4, 5)

    fmt.Println(s)
}

func main() {
    nums := make([]int, 0, 3)
    fmt.Println(nums, len(nums), cap(nums))

    f(nums)

    fmt.Println(nums, len(nums), cap(nums))
    fmt.Println(nums[:3])
}

那么这段代码输出多少呢?

[] 0 3
[4 5]
[] 0 3
[4 5 0]

解析:

在 Go 语言中,只有值传递

要记住一个关键点:如果传过去的值是指向内存空间的地址,是可以对这块内存空间做修改的。反之,你也改不了

实质上在调用f(nums) 函数时,实际上修改了底层所指向的数组。

疑问点❓

为何fmt.Println(nums)fmt.Println(nums[:5])打印的结果差别这么大?

  1. 在调用函数f时修改了底层数组,但未修改nums的长度(len(nums)依然是0),相对的fmt.Println(nums) 因为该切片 len 值为 0,没有指定最大索引值,high 则取 len 值,导致输出结果为空。
  2. nums[:5] 截取表达式 s[low : high]中的 high,最大的取值范围对应着切片的容量,不是单纯的长度。因此调用 fmt.Println(nums[:5]) 时可以输出容量范围内的值,不会出现越界。


截取

func main() {
    nums := []int{1, 2, 3, 4, 5}
    b := nums[2:4]
    b[0] = 10

    fmt.Println(nums)
    fmt.Println(b)
}

那么这段代码输出多少呢?

[1 2 10 4 5]
[10 4]

解析:

GO 中对切片进行切割, 并不会切一个新的数组出来, 而是仍然使用原数组, 只是修改下数组的首地址和长度。

所以go 切片切割仍然使用的是原数组,所以改变b时也改变了a,而b索引位0 指向的是a 索引位2,所以a最终结果是1 2 10 4 5。


函数操作

func set(nums []int, v int) {
    nums[3] = v
}

func main() {
    nums := []int{1, 2, 3, 4, 5}
    set(nums, 6)

    fmt.Println(nums)
}

那么这段代码输出多少呢?

[1 2 3 6 5]

解析:

如果传过去的值是指向内存空间的地址,那么是可以对这块内存空间做修改的。

所以在函数set()中修改nums索引位3的数据,外部的nums也会被修改。

疑问点❓

为什么 slice 类型可以直接修改源数据的值呢?

其实和输出的原理是一样的,在 Go 语言运行时,传递的也是相应 slice 类型的底层数组的指针,但需要注意,其使用的是指针的副本。严格意义是引用类型,依旧是值传递。


defer与return

  1. 想想下面这个输出多少?

    func main() {
         fmt.Println("return:", testD1())
    }
    func testD1() int {
         defer fmt.Println("defer1")
    
         i := 1
         defer func() {
             i++
             fmt.Println("defer-i", i)
         }()
    
         fmt.Println(i)
    
         return i
    }

    输出结果

     1
     defer-i 2
     defer1
     return: 1

    这个例子很简单,就是按照 defer 先进后出的执行顺序执行的。

  2. 那么在想想这个例子呢?

    func main() {
         fmt.Println("return:", testD2())
    }
    func testD2() (i int) {
         defer fmt.Println("defer1")
    
         i = 1
         defer func() {
             i++
             fmt.Println("defer-i", i)
         }()
    
         fmt.Println(i)
    
         return
    }

    输出结果

     1
     defer-i 2
     defer1
     return: 2

    为什么第二次 return: 2 返回值变成2呢?
    因为:

    • return语句本身并不是一条原子指令,它会先给返回值赋值,然后再是返回
    • 在含defer表达式时,函数返回的过程是这样的:先给返回值赋值,然后调用defer表达式,最后再是返回结果
    • 但对于匿名和有名返回值时,return会将返回值先保存起来,对于匿名返回值来说,保存在一个临时对象中,defer是看不到这个临时对象的;而对于有名返回值来说,就保存在已命名的变量中

Map

func test1(m map[string]interface{}) {
    m["name"] = "张三"
}

func main() {
    m := make(map[string]interface{})
    m["age"] = 23
    m["name"] = "李四"
    test1(m)
    fmt.Println(m)
}

那么这段代码输出多少呢?

map[age:23 name:张三]

为什么在调用 test1()name 的值就变成了 「张三」了呢?

解释:

func makemap(t *maptype, hint int, h *hmap) *hmap {}

这是创建 map 类型的底层方法,其返回的是 *hmap 类型,是一个指针。

就是说当我们在调用 test1方法时,其相当于是在传入一个指针参数test1(*hmap),从而修改了map中的name值。


总结

以上内容就是今天要分享的面试题了,再次强调的是 slice 部分,就是面试官口中的:“你在使用切片时遇到过哪些坑呢?”

希望这次分享能帮助大家拿到满意的offer!!!



END

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 1

已收藏

1个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!