Go语言切片面试真题8连问

2024-03-17 08:40

本文主要是介绍Go语言切片面试真题8连问,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

前言

哈喽,大家好,我是asong。最近没事在看八股文,总结了几道常考的切片八股文,以问答的方式总结出来,希望对正在面试的你们有用~

本文题目不全,关于切片的面试真题还有哪些?欢迎评论区补充~

01. 数组和切片有什么区别?

Go语言中数组是固定长度的,不能动态扩容,在编译期就会确定大小,声明方式如下:

var buffer [255]int
buffer := [255]int{0}

切片是对数组的抽象,因为数组的长度是不可变的,在某些场景下使用起来就不是很方便,所以Go语言提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素。切片是一种数据结构,切片不是数组,切片描述的是一块数组,切片结构如下:

aee792c0f12a220b0548f799f1d29d2a.png

我们可以直接声明一个未指定大小的数组来定义切片,也可以使用make()函数来创建切片,声明方式如下:

var slice []int // 直接声明
slice := []int{1,2,3,4,5} // 字面量方式
slice := make([]int, 5, 10) // make创建
slice := array[1:5] // 截取下标的方式
slice := *new([]int) // new一个

切片可以使用append追加元素,当cap不足时进行动态扩容。

02. 拷贝大切片一定比拷贝小切片代价大吗?

这道题比较有意思,原文地址:Are large slices more expensive than smaller ones?

这道题本质是考察对切片本质的理解,Go语言中只有值传递,所以我们以传递切片为例子:

func main()  {param1 := make([]int, 100)param2 := make([]int, 100000000)smallSlice(param1)largeSlice(param2)
}func smallSlice(params []int)  {// ....
}func largeSlice(params []int)  {// ....
}

切片param2要比param11000000个数量级,在进行值拷贝的时候,是否需要更昂贵的操作呢?

实际上不会,因为切片本质内部结构如下:

type SliceHeader struct {Data uintptrLen  intCap  int
}

切片中的第一个字是指向切片底层数组的指针,这是切片的存储空间,第二个字段是切片的长度,第三个字段是容量。将一个切片变量分配给另一个变量只会复制三个机器字,大切片跟小切片的区别无非就是 LenCap的值比小切片的这两个值大一些,如果发生拷贝,本质上就是拷贝上面的三个字段。

03. 切片的深浅拷贝

深浅拷贝都是进行复制,区别在于复制出来的新对象与原来的对象在它们发生改变时,是否会相互影响,本质区别就是复制出来的对象与原对象是否会指向同一个地址。在Go语言,切片拷贝有三种方式:

  • 使用=操作符拷贝切片,这种就是浅拷贝

  • 使用[:]下标的方式复制切片,这种也是浅拷贝

  • 使用Go语言的内置函数copy()进行切片拷贝,这种就是深拷贝,

04. 零切片、空切片、nil切片是什么

为什么问题中这么多种切片呢?因为在Go语言中切片的创建方式有五种,不同方式创建出来的切片也不一样;

  • 零切片

我们把切片内部数组的元素都是零值或者底层数组的内容就全是 nil的切片叫做零切片,使用make创建的、长度、容量都不为0的切片就是零值切片:

slice := make([]int,5) // 0 0 0 0 0
slice := make([]*int,5) // nil nil nil nil nil
  • nil切片

nil切片的长度和容量都为0,并且和nil比较的结果为true,采用直接创建切片的方式、new创建切片的方式都可以创建nil切片:

var slice []int
var slice = *new([]int)
  • 空切片

空切片的长度和容量也都为0,但是和nil的比较结果为false,因为所有的空切片的数据指针都指向同一个地址 0xc42003bda0;使用字面量、make可以创建空切片:

var slice = []int{}
var slice = make([]int, 0)

空切片指向的 zerobase 内存地址是一个神奇的地址,从 Go 语言的源代码中可以看到它的定义:

// base address for all 0-byte allocations
var zerobase uintptr// 分配对象内存
func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {...if size == 0 {return unsafe.Pointer(&zerobase)}...
}

05. 切片的扩容策略

这个问题是一个高频考点,我们通过源码来解析一下切片的扩容策略,切片的扩容都是调用growslice方法,截取部分重要源代码:

// runtime/slice.go
// et:表示slice的一个元素;old:表示旧的slice;cap:表示新切片需要的容量;
func growslice(et *_type, old slice, cap int) slice {if cap < old.cap {panic(errorString("growslice: cap out of range"))}if et.size == 0 {// append should not create a slice with nil pointer but non-zero len.// We assume that append doesn't need to preserve old.array in this case.return slice{unsafe.Pointer(&zerobase), old.len, cap}}newcap := old.cap// 两倍扩容doublecap := newcap + newcap// 新切片需要的容量大于两倍扩容的容量,则直接按照新切片需要的容量扩容if cap > doublecap {newcap = cap} else {// 原 slice 容量小于 1024 的时候,新 slice 容量按2倍扩容if old.cap < 1024 {newcap = doublecap} else { // 原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。// 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}}}// 后半部分还对 newcap 作了一个内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于 老 slice 容量的 2倍或者1.25倍。var overflow boolvar lenmem, newlenmem, capmem uintptr// Specialize for common values of et.size.// For 1 we don't need any division/multiplication.// For sys.PtrSize, compiler will optimize division/multiplication into a shift by a constant.// For powers of 2, use a variable shift.switch {case et.size == 1:lenmem = uintptr(old.len)newlenmem = uintptr(cap)capmem = roundupsize(uintptr(newcap))overflow = uintptr(newcap) > maxAllocnewcap = int(capmem)case et.size == sys.PtrSize:lenmem = uintptr(old.len) * sys.PtrSizenewlenmem = uintptr(cap) * sys.PtrSizecapmem = roundupsize(uintptr(newcap) * sys.PtrSize)overflow = uintptr(newcap) > maxAlloc/sys.PtrSizenewcap = int(capmem / sys.PtrSize)case isPowerOfTwo(et.size):var shift uintptrif sys.PtrSize == 8 {// Mask shift for better code generation.shift = uintptr(sys.Ctz64(uint64(et.size))) & 63} else {shift = uintptr(sys.Ctz32(uint32(et.size))) & 31}lenmem = uintptr(old.len) << shiftnewlenmem = uintptr(cap) << shiftcapmem = roundupsize(uintptr(newcap) << shift)overflow = uintptr(newcap) > (maxAlloc >> shift)newcap = int(capmem >> shift)default:lenmem = uintptr(old.len) * et.sizenewlenmem = uintptr(cap) * et.sizecapmem, overflow = math.MulUintptr(et.size, uintptr(newcap))capmem = roundupsize(capmem)newcap = int(capmem / et.size)}
}

通过源代码可以总结切片扩容策略:

切片在扩容时会进行内存对齐,这个和内存分配策略相关。进行内存对齐之后,新 slice 的容量是要 大于等于老 slice 容量的 2倍或者1.25倍,当原 slice 容量小于 1024 的时候,新 slice 容量变成原来的 2 倍;原 slice 容量超过 1024,新 slice 容量变成原来的1.25倍。

07. 参数传递切片和切片指针有什么区别?

我们都知道切片底层就是一个结构体,里面有三个元素:

type SliceHeader struct {Data uintptrLen  intCap  int
}

分别表示切片底层数据的地址,切片长度,切片容量。

当切片作为参数传递时,其实就是一个结构体的传递,因为Go语言参数传递只有值传递,传递一个切片就会浅拷贝原切片,但因为底层数据的地址没有变,所以在函数内对切片的修改,也将会影响到函数外的切片,举例:

func modifySlice(s []string)  {s[0] = "song"s[1] = "Golang"fmt.Println("out slice: ", s)
}func main()  {s := []string{"asong", "Golang梦工厂"}modifySlice(s)fmt.Println("inner slice: ", s)
}
// 运行结果
out slice:  [song Golang]
inner slice:  [song Golang]

不过这也有一个特例,先看一个例子:

func appendSlice(s []string)  {s = append(s, "快关注!!")fmt.Println("out slice: ", s)
}func main()  {s := []string{"asong", "Golang梦工厂"}appendSlice(s)fmt.Println("inner slice: ", s)
}
// 运行结果
out slice:  [asong Golang梦工厂 快关注!!]
inner slice:  [asong Golang梦工厂]

因为切片发生了扩容,函数外的切片指向了一个新的底层数组,所以函数内外不会相互影响,因此可以得出一个结论,当参数直接传递切片时,如果指向底层数组的指针被覆盖或者修改(copy、重分配、append触发扩容),此时函数内部对数据的修改将不再影响到外部的切片,代表长度的len和容量cap也均不会被修改

参数传递切片指针就很容易理解了,如果你想修改切片中元素的值,并且更改切片的容量和底层数组,则应该按指针传递。

08. range遍历切片有什么要注意的?

Go语言提供了range关键字用于for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素,有两种使用方式:

for k,v := range _ { }
for k := range _ { }

第一种是遍历下标和对应值,第二种是只遍历下标,使用range遍历切片时会先拷贝一份,然后在遍历拷贝数据:

s := []int{1, 2}
for k, v := range s {}
会被编译器认为是
for_temp := s
len_temp := len(for_temp)
for index_temp := 0; index_temp < len_temp; index_temp++ {value_temp := for_temp[index_temp]_ = index_tempvalue := value_temp}

不知道这个知识点的情况下很容易踩坑,例如下面这个例子:

package mainimport ("fmt"
)type user struct {name stringage uint64
}func main()  {u := []user{{"asong",23},{"song",19},{"asong2020",18},}for _,v := range u{if v.age != 18{v.age = 20}}fmt.Println(u)
}
// 运行结果
[{asong 23} {song 19} {asong2020 18}]

因为使用range遍历切片u,变量v是拷贝切片中的数据,修改拷贝数据不会对原切片有影响。

之前写了一个对for-range踩坑总结,可以读一下:面试官:go中for-range使用过吗?这几个问题你能解释一下原因吗?

总结

本文总结了8道切片相关的面试真题,切片一直是面试中的重要考点,把本文这几个知识点弄会,应对面试官就会变的轻松自如。

关于切片的面试真题还有哪些?欢迎评论区补充~

好啦,本文到这里就结束了,我是asong,我们下期见。

创建了读者交流群,欢迎各位大佬们踊跃入群,一起学习交流。入群方式:关注公众号获取。更多学习资料请到公众号领取。


这篇关于Go语言切片面试真题8连问的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/818435

相关文章

字节面试 | 如何测试RocketMQ、RocketMQ?

字节面试:RocketMQ是怎么测试的呢? 答: 首先保证消息的消费正确、设计逆向用例,在验证消息内容为空等情况时的消费正确性; 推送大批量MQ,通过Admin控制台查看MQ消费的情况,是否出现消费假死、TPS是否正常等等问题。(上述都是临场发挥,但是RocketMQ真正的测试点,还真的需要探讨) 01 先了解RocketMQ 作为测试也是要简单了解RocketMQ。简单来说,就是一个分

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

Go Playground 在线编程环境

For all examples in this and the next chapter, we will use Go Playground. Go Playground represents a web service that can run programs written in Go. It can be opened in a web browser using the follow

go基础知识归纳总结

无缓冲的 channel 和有缓冲的 channel 的区别? 在 Go 语言中,channel 是用来在 goroutines 之间传递数据的主要机制。它们有两种类型:无缓冲的 channel 和有缓冲的 channel。 无缓冲的 channel 行为:无缓冲的 channel 是一种同步的通信方式,发送和接收必须同时发生。如果一个 goroutine 试图通过无缓冲 channel

C语言 | Leetcode C语言题解之第393题UTF-8编码验证

题目: 题解: static const int MASK1 = 1 << 7;static const int MASK2 = (1 << 7) + (1 << 6);bool isValid(int num) {return (num & MASK2) == MASK1;}int getBytes(int num) {if ((num & MASK1) == 0) {return

MiniGPT-3D, 首个高效的3D点云大语言模型,仅需一张RTX3090显卡,训练一天时间,已开源

项目主页:https://tangyuan96.github.io/minigpt_3d_project_page/ 代码:https://github.com/TangYuan96/MiniGPT-3D 论文:https://arxiv.org/pdf/2405.01413 MiniGPT-3D在多个任务上取得了SoTA,被ACM MM2024接收,只拥有47.8M的可训练参数,在一张RTX

如何确定 Go 语言中 HTTP 连接池的最佳参数?

确定 Go 语言中 HTTP 连接池的最佳参数可以通过以下几种方式: 一、分析应用场景和需求 并发请求量: 确定应用程序在特定时间段内可能同时发起的 HTTP 请求数量。如果并发请求量很高,需要设置较大的连接池参数以满足需求。例如,对于一个高并发的 Web 服务,可能同时有数百个请求在处理,此时需要较大的连接池大小。可以通过压力测试工具模拟高并发场景,观察系统在不同并发请求下的性能表现,从而

C语言:柔性数组

数组定义 柔性数组 err int arr[0] = {0}; // ERROR 柔性数组 // 常见struct Test{int len;char arr[1024];} // 柔性数组struct Test{int len;char arr[0];}struct Test *t;t = malloc(sizeof(Test) + 11);strcpy(t->arr,