author:魏静崎
2024年10月15日
Golang零散面试题
注:来源包括自身面试经历,网络资料,包括但不限于golang知识点,持续更新ing等
Go基础
int和int32的区别
rune就是int32。int可能是32位的也可能是64位的,并不是int32的别名。
make和new的区别
- new给string、int和数组分配内存,make给切片、map、channel分配内存。
- new 的作用是初始化一个指向类型的指针(*T),make 的作用是为 slice,map 或 chan 初始化并返回引用(T),即变量本身。
- new分配的空间被清零,make分配空间后,会进行初始化。
数组和切片的区别?作为参数的区别?
相同点:
- 只能存储一组相同类型的数据结构
- 都是通过下标来访问,并且有容量长度,长度通过 len 获取,容量通过 cap 获取
区别: - 数组是定长,切片可以自动扩容
- 数组是值类型,切片是引用类型
- 切片本身不能存储任何数据,都是底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据。切片一旦扩容,指向一个新的底层数组,内存地址也就随之改变
作为参数的区别:- 作为参数,数组是值传递,在函数内部对数组进行修改并不会影响原数据
- 切片也是值传递,但是切片的值就是对应底层数组的地址,因此修改切片的时候,源数据也会被修改,如果不希望源数据被修改话的我们可以使用copy函数复制切片后再传入
Go 的 slice 底层数据结构和一些特性
Go 的 slice 底层数据结构是由一个 array 指针指向底层数组,len 表示切片长度,cap 表示切片容量。
扩容规则:对于切片的扩容规则:当切片比较小时(容量小于 1024),新的扩容会是原来的 2 倍,避免频繁扩容,从而减少内存分配的次数和数据拷贝的代价。当切片较大的时(原来的 slice 的容量大于或者等于 1024),采用较小的扩容倍速(新的扩容将扩大大于或者等于原来 1.25 倍),主要避免空间浪费 。实际上还要考虑内存对齐,扩容是大于或者等于 1.25 倍。
指针传递的内存逃逸
- 本该分配到栈上的变量,跑到了堆上,这就导致了内存逃逸。
- 栈是高地址到低地址,栈上的变量,函数结束后变量会跟着回收掉,不会有额外性能的开销。
- 变量从栈逃逸到堆上,如果要回收掉,需要进行 gc,那么 gc 一定会带来额外的性能开销。
内存逃逸的情况:
方法内返回局部指针变量
向channel发送指针数据
在闭包中引用包外的值
在slice或map中存储指针
切片扩容后长度太大
在interface类型上调用方法
for range时地址会发生变化吗
在for a, b range的遍历中, a 和 b 在内存中只会存在一份,即之后每次循环时遍历到的数据都是以值覆盖的方式赋给 a 和 b,a,b 的内存地址始终不变。由于有这个特性,for 循环里面如果开协程,不要直接把 a 或者 b 的地址传给协程。解决办法:在每次循环时,创建一个临时变量。
map 的数据结构是什么?
Golang中的map底层使用Hash table,用链表来解决冲突,当出现冲突时,并非每一个key都通过链表串起来,而是以bmap为最小粒度挂载,一个bmap可以存放8个k-v。
哈希函数:如果CPU支持则选择aes hash,否则选择mem Hash。
map如何扩容,除了哈希表还有其他实现吗
map扩容:
容量大小:底层调用makemap函数,计算得到合适的B,map容量最多可容纳6.52B个元素,6.5为装载因子阈值常量。
扩容条件:
负载因子 > 6.5时,也即平均每个bucket存储的键值对达到6.5个。
overflow数量 > 2^15时,也即overflow数量超过32768时。
扩容方式:
增量扩容:当负载因子过大时,就新建一个bucket,新的bucket长度是原来的2倍,然后旧bucket数据搬迁到新的bucket。 考虑到如果map存储了数以亿计的key-value,一次性搬迁将会造成比较大的延时,Go采用逐步搬迁策略,即每次访问map时都会触发一次搬迁,每次搬迁2个键值对。
等量扩容:实际上并不是扩大容量,buckets数量不变,重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次,以使bucket的使用率更高,进而保证更快的存取。
调用函数传入结构体时,应该传值还是指针?
Go 的函数参数传递都是值传递。所谓值传递:指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。参数传递还有引用传递,所谓引用传递是指在调用函数时将实际参数的地址传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
注:对结构体、map、数组、切片进行测试
结构体实际测试中,应该传递指针,否则不改变原有值。
对map测试,值传递也会改变原有值
对数组测试,值传递也会改变原有值
对切片测试,值传递也会改变原有值
值拷贝 与 引用拷贝,深拷贝 与 浅拷贝
本质区别:是否真正获取对象实体,而不是引用。
Go 多返回值怎么实现的?
Go 传参和返回值是通过 FP+offset 实现,并且存储在调用函数的栈帧中。FP 栈底寄存器,指向一个函数栈的顶部;PC 程序计数器,指向下一条执行指令;SB 指向静态数据的基指针,全局符号;SP 栈顶寄存器。
什么是 GMP?
GMP 模型是 Go 语言运行时用于管理 goroutine 的调度模型。GMP 分别代表 Goroutines (G)、Machine (M) 和 Processor (P)。
GMP 模型的工作原理:
当一个 goroutine 被创建时,它会被放入到某个 P 的本地队列中。
P 从本地队列中取出 goroutine 并分配给 M 执行。
如果一个 P 的本地队列为空,会从其他 P 的队列中窃取任务,保证工作负载均衡。
当一个 goroutine 发生阻塞时,M 会释放 P,去寻找其他可运行的 goroutine,避免资源浪费。
协程原理、发生阻塞怎么办、协程和协程状态都有什么、协程和线程占用的内存、一个协程一直等待怎么办
channel有无缓冲区的区别
无缓存:当缓存为0时,数据直接从发送者传递到接收者。发送和接收是同步的,只有在接收者准备好接收数据时,发送操作才会继续。否则,发送者会阻塞等待接收者。适用于需要严格同步的情况,确保发送和接收操作严格配对执行。
有缓存:可以存储多条未被接收的数据。发送操作不会立即阻塞,只有当缓存空间满时才会阻塞。适用于高效的数据传递,缓冲可以让发送者无需等待接收者,从而提高并发效率。
channel的底层实现,如数据结构是什么
channel
是用于 goroutine 之间通信的核心机制,底层是 基于队列的同步数据结构,可以理解为一个 环形队列 + 互斥锁 + 条件变量 的组合。
其中一个线程or协程发生OOM(Out of Memory)会发生什么,怎么解决
在 Go 语言中,如果 某个线程(OS 线程)或 Goroutine 发生 OOM(Out of Memory,内存溢出),整个 Go 进程会崩溃,并抛出 runtime: out of memory
错误。OOM 可能发生在以下场景:
- 创建过多 Goroutine
- 内存泄漏(Memory Leak)
- 大对象分配失败
- 死循环分配内存
- 垃圾回收未及时释放内存
解决方案:限制 Goroutine、监控内存、手动 GC、限制内存使用、自动重启进程。
了解哪些锁,互斥锁和自旋锁区别及适用场景
互斥锁是一种并发编程中常用的同步机制,用于保护共享资源的访问。
在Go语言中,可以使用sync包中的Mutex类型来实现互斥锁。通过调用Lock方法来获取锁,保护共享资源的访问,然后在使用完共享资源后调用Unlock方法释放锁。
自旋状态是并发编程中的一种状态,指的是线程或进程在等待某个条件满足时,不会进入休眠或阻塞状态,而是通过不断地检查条件是否满足来进行忙等待。
go defer,多个 defer 的顺序,defer 在什么时机会修改返回值?defer 底层数据结构和一些特性?
多个 defer 调用顺序是 LIFO(后入先出),defer可以修改函数最终返回值,修改时机:有名返回值或者函数返回指针。
每个 defer 语句都对应一个_defer 实例,多个实例使用指针连接起来形成一个单连表,保存在 gotoutine 数据结构中,每次插入_defer 实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果。
Go出现panic的场景
● 数组/切片越界
● 空指针调用。比如访问一个 nil 结构体指针的成员
● 过早关闭 HTTP 响应体
● 除以 0
● 向已经关闭的 channel 发送消息
● 重复关闭 channel
● 关闭未初始化的 channel
● 未初始化 map。注意访问 map 不存在的 key 不会 panic,而是返回 map 类型对应的零值,但是不能直接赋值
● 跨协程的 panic 处理
● sync 计数为负数。
● 类型断言不匹配。
基本数据类型的并发安全性,管道是否并发安全
基本数据类型(如整数、布尔值、字符串等)本身是线程安全的,但如果多个 goroutine 同时读写同一个变量,就会出现竞态条件。这种情况下,你需要使用同步机制,如互斥锁(sync.Mutex
)或通道(chan
)来确保并发安全。struct
结构体本身并不是并发安全的。
Go语言中的map并发安全性
map类型并不是并发安全的。这意味着,如果有多个goroutine尝试同时读写同一个map,可能会导致竞态条件和数据损坏。
解决方案:
使用互斥锁(sync.Mutex):在读写map的操作前后加锁,确保同一时间只有一个goroutine可以访问map。
使用读写互斥锁(sync.RWMutex):如果读操作远多于写操作,可以使用读写锁来提高性能。读写锁允许多个goroutine同时读取map,但在写入时需要独占访问。
使用并发安全的map(sync.Map):从Go 1.9版本开始,标准库中的sync包提供了sync.Map类型,这是一个专为并发环境设计的map。它提供了一系列方法来安全地在多个goroutine之间共享数据。
sync.map底层实现
- 通过 read 和 dirty 两个字段将读写分离,读的数据存在只读字段 read 上,将最新写入的数据则存在 dirty 字段上
- 读取时会先查询 read,不存在再查询 dirty,写入时则只写入 dirty
- 读取 read 并不需要加锁,而读或写 dirty 都需要加锁
- 另外有 misses 字段来统计 read 被穿透的次数(被穿透指需要读 dirty 的情况),超过一定次数则将 dirty 数据同步到 read 上
- 对于删除数据则直接通过标记来延迟删除
Channel 是否线程安全?锁用在什么地方?
Golang的Channel,发送一个数据到Channel 和 从Channel接收一个数据 都是 原子性的。
Go的设计思想就是:不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。
Go项目
context 结构是什么样的?context 使用场景和用途?在go中一般可以用来做什么?
Go 的 Context 的数据结构包含 Deadline,Done,Err,Value。
Deadline 方法返回一个 time.Time,表示当前 Context 应该结束的时间,ok 则表示有结束时间,
Done 方法当 Context 被取消或者超时时候返回的一个 close 的 channel,告诉给 context 相关的函数要停止当前工作然后返回了,
Err 表示 context 被取消的原因,
Value 方法表示 context 实现共享数据存储的地方,是协程安全的。
context 在业务中是经常被使用的, 其主要的应用 : 1:上下文控制,2:多个 goroutine 之间的数据交互等,3:超时控制:到某个时间点超时,过多久超时。
context 包提供了一种管理多个 goroutine 之间的截止时间、取消信号和请求范围数据的方法。
项目中的错误如何处理
若干个协程,其中一个发生panic会发生什么
- **一个
goroutine
发生panic
,不会影响其他goroutine
**(除非它影响了共享资源)。 - 如果
main
goroutine
发生panic
,整个程序会崩溃,因为main
退出会导致所有goroutine
终止。 - 如果
panic
没有被recover
捕获,整个程序会崩溃。
defer可以捕获到其goroutine的子goroutine的panic吗
不能
Gin的一些底层原理
在 Gin 框架中,HTTP 协议的封装主要通过 gin.Context
结构体来实现。以下是对 Gin 如何封装 HTTP 协议的详细解释:
1. 请求和响应的封装
- **请求 (Request)**:
- Gin 使用
http.Request
结构体来表示客户端发起的 HTTP 请求。通过gin.Context
,开发者可以方便地访问请求头、请求体、查询参数、路径参数等。 - 在 Gin 中,
gin.Context
中封装了*http.Request
,使得开发者能够通过c.Request
访问底层的 HTTP 请求。
- Gin 使用
- **响应 (Response)**:
- Gin 使用
http.ResponseWriter
结构体来控制 HTTP 响应。gin.Context
同样封装了http.ResponseWriter
,让开发者可以方便地构建和发送响应。 - 提供了一些简化方法,例如
c.JSON()
、c.String()
、c.HTML()
等,帮助开发者快速生成 JSON、字符串和 HTML 响应。
- Gin 使用
2. 中间件的支持
- Gin 支持 HTTP 中间件,通过
c.Next()
和c.Abort()
方法可以控制请求的处理流程。这种设计允许在请求处理链中插入自定义逻辑(例如身份验证、日志记录、错误处理等),从而增强应用的可扩展性。
3. 路由的封装
- Gin 提供了一种简单的方式来定义路由。它使用
gin.Engine
结构体来管理路由和中间件。 - 每个路由都与 HTTP 方法(如 GET、POST、PUT、DELETE)相对应,开发者可以方便地注册各种路由处理程序。
4. JSON 和数据绑定
- Gin 封装了 HTTP 请求的数据绑定功能,支持从请求中直接将 JSON 数据绑定到结构体。开发者只需调用
c.BindJSON(&someStruct)
,Gin 会自动处理 JSON 解码和绑定。 - 对于表单数据和查询参数的绑定,Gin 也提供了类似的函数 (
c.ShouldBindQuery()
、c.ShouldBindForm()
)。
5. 错误处理
- Gin 提供了统一的错误处理机制。在中间件或路由处理函数中,开发者可以通过
c.Error(err)
方法记录错误。Gin 会将这些错误信息捕获并处理。
Gin如何做参数校验,中间件如何使用
使用的是bind类中的方法进行参数校验和绑定
反射及其原理,实现根据字符串函数名调用函数
反射是一种检查interface变量的底层类型和值的机制,interface类型有个(value,type)对,而 反射就是检查interface的这个(value, type)对的。
负载均衡算法、简单实现
轮训、加权轮训、随机、最短响应时间优先
Else
分布式一致性
保证集群数据的一致性,一般要达到一半以上的节点数据写入成功。
强一致性:保证数据都一样才返回确认。弱一致性:写入单个成功,开始异步同步就返回确认。
一致性协议Raft: 共识算法的通用特点:半数以上的节点达成共识
优势:
强领导者:只能存在一个leader,所有写操作都只能通过leader完成,数据只能从leader流向follower节点,无论集群有多大,始终只有leader可以处理写请求
领导者选举:随机化定时器选举leader
成员变更:联合共识算法
编程题
并发编程
1、有三个函数,分别可以打印”cat”,”dog”,”fish”。 每个函数都使用一个协程,按照上述的顺序打印一百次。
定义三个管道(应该也可以定义一个缓存为3的管道),每个协程传入一个对应的信号量时下一个协程继续运行,下一个协程接收到信号量后开始运行本次循环,以此类推。
1 | func dog(counter uint64, quitch chan struct{}, dogch, fishch chan struct{}) { |
使用空结构体因为其占用内存最少,应该可以,但是似乎这样会多输出一次dog。
2、n个生产者,m个消费者,写一个shutdown函数,实现关闭生产者并让消费者读完channel中的数据后关闭。
12. 持续生产的生产者:
- producer
函数中使用一个无限循环,通过 select
语句监听 shutdown
通道。
- 当收到关闭信号时,会打印接收到信号的消息并返回,从而结束生产。
- 否则,继续生成数据,并将其发送到 jobs
通道。
13. 关闭信号通道:
- shutdownChan
通道用于发送关闭信号,通知所有生产者停止生产。
- shutdown
函数在确认所有生产者停止后,依次关闭 jobs
通道和 shutdownChan
通道。
14. 消费者:
- 消费者仍然是从 jobs
通道中接收数据并进行处理,处理完成后,由于通道的关闭,消费者将自动停止。
15. 主函数:
- 在主函数中,启动了固定数量的生产者和消费者。
- 通过 time.Sleep
延迟,模拟了一段时间后发送关闭信号。
1 | // 生产者函数 |
3、100个生产者,最多10个消费者,主线程如何调度来保证资源合理利用
我当时想的是刚开始只启用一个消费者,使用一个额外的管道,如果当前消费者无法从生产者的管道中获取数据,则写入信号量到额外的管道中,调度线程如果拿到了该信信号量则判定消费者数量足够,如果长期没有拿到该信号量则新增消费者直到拿到该信号量或达到资源上限。
反问问题
1. 部门具体是做什么的
2. 岗位职责是什么
3. 技术栈主要是什么
4. 工作的时间制度
5. 经常加班吗
- 本文作者: 魏静崎
- 本文链接: https://slightwjq.github.io/2024/10/17/Go面试题/
- 版权声明: 该文章来源及最终解释权归作者所有