Golang常见面试题
文章目录
- Go 面试问题及答案
- 基础相关
- 1. golang 中 make 和 new 的区别?
- 2. 数组和切片的区别
- 3. for range 的时候它的地址会发生变化么?
- 4. go defer 的顺序和返回值修改
- 5. uint 类型溢出
- 6. 介绍 rune 类型
- 7. golang 中解析 tag 和反射原理
- 8. 调用函数传入结构体时,应该传值还是指针?
- 9. slice 遇到过哪些坑?
- 10. go struct 能不能比较?
- 11. Go 闭包
- Context 相关
- 1. context 结构是什么样的?
- 2. context 使用场景和用途?
- Channel 相关
- 1. channel 是否线程安全?
- 2. go channel 的底层实现原理
- 3. nil、关闭的 channel、有数据的 channel 的读写行为
- 4. 向 channel 发送数据和从 channel 读数据的流程
- Map 相关
- 1. map 使用注意点,并发安全?
- 2. map 循环是有序的还是无序的?
- 3. map 中删除一个 key,它的内存会释放么?
- 4. 处理 map 并发访问的方案
- 5. nil map 和空 map 的区别
- 6. map 的数据结构和扩容
- 7. map 取一个 key 后修改值,原 map 是否变化?
- GMP 相关
- 1. 什么是 GMP?
- 2. 进程、线程、协程的区别
- 3. 抢占式调度如何实现?
- 4. M 和 P 的数量问题
- 5. 协程怎么退出?
- 6. map 如何顺序读取?
- 锁相关
- 1. 除了 mutex 以外的安全读写共享变量方式
- 2. Go 如何实现原子操作?
- 3. Mutex 是悲观锁还是乐观锁?
- 4. Mutex 有几种模式?
- 5. goroutine 的自旋占用资源如何解决?
- 6. 读写锁底层实现
- 同步原语相关
- 1. sync 同步原语
- 2. sync.WaitGroup
- 并发相关
- 1. 控制并发数
- 2. 多个 goroutine 对同一个 map 写会 panic,异常是否可以用 defer 捕获?
- 3. 如何优雅地实现一个 goroutine 池
- 4. select 可以用于什么?
- 5. 主协程如何等其余协程完再操作?
- GC 相关
- 1. go gc 是怎么实现的?
- 2. go gc 算法是怎么实现的?
- 3. GC 中 STW 时机,各个阶段是如何解决的?
- 4. GC 的触发时机
- 内存相关
- 1. 谈谈内存泄露,什么情况下内存会泄露?
- 2. golang 的内存逃逸
- 3. Go 的内存分配
- 4. 大对象小对象
- 5. 堆和栈的区别
- 6. 当 go 服务部署到线上发现内存泄露,该怎么处理?
- 微服务框架
- 1. go-micro 微服务架构怎么实现水平部署的,代码怎么实现?
- 2. 怎么做服务发现的
- 其他
- 1. go 实现单例的方式
- 2. 项目中使用 go 遇到的坑
- 3. client 如何实现长连接?
- 编程题
- 1. 3 个函数分别打印 cat、dog、fish,要求每个函数都要起一个 goroutine,按照 cat、dog、fish 顺序打印在屏幕上 100 次。
- 2. 如何优雅地实现一个 goroutine 池
Go 面试问题及答案
基础相关
1. golang 中 make 和 new 的区别?
-
make
: 用于初始化切片、map 和 channel。返回的是这些数据结构的引用。make
会分配内存并初始化内存内容。slice := make([]int, 0) // 创建一个空的切片 m := make(map[string]int) // 创建一个空的 map ch := make(chan int) // 创建一个 channel
-
new
: 用于分配内存,并返回指向该类型的指针,且未初始化内存内容。p := new(int) // 创建一个指向 int 的指针,初始值为 0
2. 数组和切片的区别
-
数组: 固定长度,长度是类型的一部分,一旦声明长度不可更改。
var arr [5]int // 数组长度为 5
-
切片: 动态长度,可以在运行时增加或减少,切片本质上是指向数组的一段。
slice := []int{1, 2, 3} // 切片,长度不固定
3. for range 的时候它的地址会发生变化么?
-
在
for range
循环中,使用的变量地址不会发生变化,循环体内的变量是同一个地址,可能会导致修改值时的问题。nums := []int{1, 2, 3} for i := range nums { fmt.Println(&nums[i]) // 打印每次循环中的地址 }
-
for 循环遍历 slice 的问题: 如果在循环中修改 slice 的内容,可能导致未定义的行为。
4. go defer 的顺序和返回值修改
-
defer
的执行顺序: 后进先出(LIFO),即最后一个defer
的函数会最先执行。func example() { defer fmt.Println("First") defer fmt.Println("Second") } // 输出顺序为 "Second" "First"
-
defer
修改返回值: 可以通过命名返回值修改,defer
在函数结束时执行,修改返回值。func f() (result int) { defer func() { result++ }() return 1 // 返回值为 2 }
-
defer recover
: 可以捕获异常,recover
需要在defer
函数中调用,通常用于恢复 panic 状态。func safeCall() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered from panic:", r) } }() panic("Something went wrong!") }
5. uint 类型溢出
- 当
uint
类型的值超过其最大值时,会从 0 开始重新计数,不会引发 panic。var u uint8 = 255 u++ // 结果为 0
6. 介绍 rune 类型
rune
是 Go 的内建类型,表示一个 Unicode 码点,实际上是int32
的别名,用于表示字符。var r rune = 'A' // 65
7. golang 中解析 tag 和反射原理
- 反射是通过
reflect
包实现的,可以动态获取类型信息和结构体字段标签(tag)。type User struct { Name string `json:"name"` } t := reflect.TypeOf(User{}) field, _ := t.Field(0) // 获取第一个字段的信息 tag := field.Tag.Get("json") // 获取 tag 的值
8. 调用函数传入结构体时,应该传值还是指针?
- 一般来说,对于大结构体,使用指针可以避免拷贝,提高性能;对于小结构体,传值通常足够。
type User struct { Name string } func updateUser(u *User) { u.Name = "Updated" }
9. slice 遇到过哪些坑?
- 当对 slice 进行扩展时,底层数组可能会重新分配,导致原 slice 的引用失效。
s1 := []int{1, 2, 3} s2 := append(s1, 4) // 如果扩展,则 s1 可能与 s2 指向不同的底层数组
10. go struct 能不能比较?
- 如果结构体中所有字段都是可比较的(如没有包含 slice、map、function),那么可以直接比较。
type Point struct { X, Y int } p1 := Point{1, 2} p2 := Point{1, 2} fmt.Println(p1 == p2) // true
11. Go 闭包
- 闭包是指能够捕获其外部变量的函数,即使外部函数已返回,闭包仍然可以访问这些变量。
func makeCounter() func() int { count := 0 return func() int { count++ return count } } counter := makeCounter() fmt.Println(counter()) // 1 fmt.Println(counter()) // 2
Context 相关
1. context 结构是什么样的?
context
是一个携带超时、取消信号、请求范围内数据的上下文,常用于控制 goroutine 的生命周期。ctx, cancel := context.WithCancel(context.Background()) defer cancel()
2. context 使用场景和用途?
- 常用于 API 请求的超时控制、取消信号的传递、请求范围内数据的传递,避免 goroutine 泄露。
Channel 相关
1. channel 是否线程安全?
- 是的,channel 是线程安全的,可以在多个 goroutine 中安全地读写。
2. go channel 的底层实现原理
- Channel 基于循环队列和互斥锁实现,支持并发安全的读写。
3. nil、关闭的 channel、有数据的 channel 的读写行为
- 读
nil
的 channel 会阻塞。 - 写
nil
的 channel 会导致 panic。 - 读关闭的 channel 会返回零值。
- 写关闭的 channel 会导致 panic。
- 从空的 channel 读取会阻塞。
4. 向 channel 发送数据和从 channel 读数据的流程
- 发送数据时,若 channel 已满则阻塞,若有协程正在等待接收则立即传递。
- 读取数据时,若 channel 为空则阻塞,若有数据则立即接收。
Map 相关
1. map 使用注意点,并发安全?
- Go 的 map 不是并发安全的,使用
sync.Mutex
或sync.Map
来保证安全。
2. map 循环是有序的还是无序的?
- map 的循环是无序的,每次遍历的顺序可能不同。
3. map 中删除一个 key,它的内存会释放么?
- 是的,删除 key 后会释放内存。
4. 处理 map 并发访问的方案
- 使用
sync.Map
或在读写 map 时加锁来处理并发访问。
5. nil map 和空 map 的区别
nil
map 不能插入数据,空 map 可以进行插入和读取。
6. map 的数据结构和扩容
- map 是基于哈希表实现的,扩容时会创建更大的哈希表并重新分配键值对。
7. map 取一个 key 后修改值,原 map 是否变化?
- 是的,修改取出的值会影响 map 中的值,前提是通过引用修改。
GMP 相关
1. 什么是 GMP?
- GMP 是 Go 的协程调度模型,包括 Goroutine(G)、机器线程(M)和 P(处理器)。调度器根据 G 和 M 的数量动态分配。
2. 进程、线程、协程的区别
- 进程: 操作系统的基本运行单位,拥有独立的内存空间。
- 线程: 进程内的执行流,多个线程共享同一进程的内存。
- 协程: 用户态轻量级线程,由 Go 运行时调度。
3. 抢占式调度如何实现?
- Go 使用抢占式调度定期检查正在运行的 goroutine,如果运行超时,则
将其挂起,切换到其他 goroutine。
4. M 和 P 的数量问题
- M 的数量代表操作系统线程的数量,P 的数量代表 Go 运行时处理 goroutines 的数量,通常 P 的数量设置为可用 CPU 核心数。
5. 协程怎么退出?
- 协程可以通过
return
语句结束,或者通过context
的取消信号终止。
6. map 如何顺序读取?
- Go 的 map 无法保证顺序读取,可以使用 slice 结构来保存 key 的顺序。
锁相关
1. 除了 mutex 以外的安全读写共享变量方式
- 使用
sync.RWMutex
读写锁、sync.Once
进行单次初始化等。
2. Go 如何实现原子操作?
- 使用
sync/atomic
包提供的函数,如atomic.AddInt32
。
3. Mutex 是悲观锁还是乐观锁?
Mutex
是悲观锁,常用于需要保证互斥访问的场景。悲观锁在操作前就假设会发生冲突,乐观锁则假设不会发生冲突。
4. Mutex 有几种模式?
Lock
和Unlock
方法用于加锁和解锁。- 可以使用
RLock
和RUnlock
实现读写锁。
5. goroutine 的自旋占用资源如何解决?
- 自旋锁(spinlock)可以避免上下文切换,但可能导致 CPU 占用高。通过合理的锁设计和使用
sync.Mutex
来控制。
6. 读写锁底层实现
- 读写锁通过计数器实现对读操作的并发支持,写操作时需要独占锁。
同步原语相关
1. sync 同步原语
sync.Mutex
: 互斥锁sync.WaitGroup
: 等待一组 goroutine 结束sync.Once
: 只执行一次的函数
2. sync.WaitGroup
- 用于等待一组 goroutine 完成,调用
Add(n)
方法增加计数,Done()
减少计数,Wait()
阻塞直到计数为 0。
并发相关
1. 控制并发数
- 使用带缓冲的 channel 来限制并发数,或者使用
sync.WaitGroup
进行控制。
2. 多个 goroutine 对同一个 map 写会 panic,异常是否可以用 defer 捕获?
- 会 panic,但可以用
defer recover()
捕获。
3. 如何优雅地实现一个 goroutine 池
- 使用 channel 来限制 goroutine 的数量。
type WorkerPool struct {
jobs chan Job
wg sync.WaitGroup
}
func (wp *WorkerPool) Start(n int) {
for i := 0; i < n; i++ {
go func() {
for job := range wp.jobs {
job.Execute()
wp.wg.Done()
}
}()
}
}
4. select 可以用于什么?
- 在多个 channel 上等待,非阻塞的读取和写入,支持超时机制。
5. 主协程如何等其余协程完再操作?
- 使用
sync.WaitGroup
来等待所有协程完成。
GC 相关
1. go gc 是怎么实现的?
- Go 的垃圾回收使用标记-清扫算法,分为三阶段:标记、清理和压缩。
2. go gc 算法是怎么实现的?
- 采用三色标记法,分为白色、灰色和黑色对象,黑色对象代表已访问,灰色对象为待访问。
3. GC 中 STW 时机,各个阶段是如何解决的?
- STW(Stop The World)发生在标记阶段,GC 在此时暂停所有 goroutine。
4. GC 的触发时机
- 内存使用达到一定阈值时、手动调用
runtime.GC()
时。
内存相关
1. 谈谈内存泄露,什么情况下内存会泄露?
- 常见于 goroutine 泄露、未关闭的 channel 或 map。可以使用
pprof
工具进行定位。
2. golang 的内存逃逸
- 内存逃逸是指变量在堆上分配而非栈上,通常发生在闭包、函数返回值等场景。
3. Go 的内存分配
- 使用
runtime.Malloc
和runtime.Free
管理堆内存,局部变量在栈上分配。
4. 大对象小对象
- 小对象多了会导致 GC 压力,因为 GC 需要频繁运行来回收小对象。
5. 堆和栈的区别
- 栈: 由操作系统管理,速度快,生命周期短。
- 堆: 由 Go 运行时管理,适合动态分配,但速度相对慢。
6. 当 go 服务部署到线上发现内存泄露,该怎么处理?
- 通过
pprof
工具进行性能分析,找到内存泄露点,使用runtime.GC()
手动触发 GC。
微服务框架
1. go-micro 微服务架构怎么实现水平部署的,代码怎么实现?
- 使用服务注册与发现机制,通过 HTTP/gRPC 进行服务间通信,代码示例使用
go-micro
框架。
2. 怎么做服务发现的
- 使用
etcd
、Consul
等工具实现服务注册与发现,保持服务的健康状态。
其他
1. go 实现单例的方式
- 使用
sync.Once
确保单例的安全创建。
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
once.Do(func() {
instance = &Singleton{}
})
return instance
}
2. 项目中使用 go 遇到的坑
- 包管理、协程泄漏、channel 使用不当等。
3. client 如何实现长连接?
- 使用
net
包建立 TCP 连接,并保持连接活跃。
conn, err := net.Dial("tcp", "server:port")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
编程题
1. 3 个函数分别打印 cat、dog、fish,要求每个函数都要起一个 goroutine,按照 cat、dog、fish 顺序打印在屏幕上 100 次。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
chCat := make(chan struct{})
chDog := make(chan struct{})
wg.Add(3)
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
chCat <- struct{}{}
fmt.Println("cat")
chDog <- struct{}{}
}
}()
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
<-chDog
fmt.Println("dog")
chFish <- struct{}{}
}
}()
go func() {
defer wg.Done()
for i := 0; i < 100; i++ {
<-chFish
fmt.Println("fish")
}
}()
chCat <- struct{}{}
wg.Wait()
}
2. 如何优雅地实现一个 goroutine 池
- 使用带缓冲的 channel 来限制 goroutine 的数量。
type Job struct {
// Job details
}
type WorkerPool struct {
jobs chan Job
wg sync.WaitGroup
}
func (wp *WorkerPool) Start(n int) {
wp.jobs = make(chan Job, n)
for i := 0; i < n; i++ {
go func() {
for job := range wp.jobs {
// Process the job
wp.wg.Done()
}
}()
}
}
func (wp *WorkerPool) AddJob(job Job) {
wp.wg.Add(1)
wp.jobs <- job
}
func (wp *WorkerPool) Close() {
close(wp.jobs)
wp.wg.Wait()
}