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
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
- 使用 net 包建立 TCP 连接,并保持连接活跃。
- 包管理、协程泄漏、channel 使用不当等。
- 使用 sync.Once 确保单例的安全创建。
- 使用 etcd、Consul 等工具实现服务注册与发现,保持服务的健康状态。
- 使用服务注册与发现机制,通过 HTTP/gRPC 进行服务间通信,代码示例使用 go-micro 框架。
- 通过 pprof 工具进行性能分析,找到内存泄露点,使用 runtime.GC() 手动触发 GC。
- 小对象多了会导致 GC 压力,因为 GC 需要频繁运行来回收小对象。
- 使用 runtime.Malloc 和 runtime.Free 管理堆内存,局部变量在栈上分配。
- 内存逃逸是指变量在堆上分配而非栈上,通常发生在闭包、函数返回值等场景。
- 常见于 goroutine 泄露、未关闭的 channel 或 map。可以使用 pprof 工具进行定位。
- 内存使用达到一定阈值时、手动调用 runtime.GC() 时。
- STW(Stop The World)发生在标记阶段,GC 在此时暂停所有 goroutine。
- 采用三色标记法,分为白色、灰色和黑色对象,黑色对象代表已访问,灰色对象为待访问。
- Go 的垃圾回收使用标记-清扫算法,分为三阶段:标记、清理和压缩。
- 使用 sync.WaitGroup 来等待所有协程完成。
- 在多个 channel 上等待,非阻塞的读取和写入,支持超时机制。
- 使用 channel 来限制 goroutine 的数量。
- 会 panic,但可以用 defer recover() 捕获。
- 使用带缓冲的 channel 来限制并发数,或者使用 sync.WaitGroup 进行控制。
- 用于等待一组 goroutine 完成,调用 Add(n) 方法增加计数,Done() 减少计数,Wait() 阻塞直到计数为 0。
- 读写锁通过计数器实现对读操作的并发支持,写操作时需要独占锁。
- 自旋锁(spinlock)可以避免上下文切换,但可能导致 CPU 占用高。通过合理的锁设计和使用 sync.Mutex 来控制。
- Mutex 是悲观锁,常用于需要保证互斥访问的场景。悲观锁在操作前就假设会发生冲突,乐观锁则假设不会发生冲突。
- 使用 sync/atomic 包提供的函数,如 atomic.AddInt32。
- 使用 sync.RWMutex 读写锁、sync.Once 进行单次初始化等。
- Go 的 map 无法保证顺序读取,可以使用 slice 结构来保存 key 的顺序。
- 协程可以通过 return 语句结束,或者通过 context 的取消信号终止。
- M 的数量代表操作系统线程的数量,P 的数量代表 Go 运行时处理 goroutines 的数量,通常 P 的数量设置为可用 CPU 核心数。
- Go 使用抢占式调度定期检查正在运行的 goroutine,如果运行超时,则
- GMP 是 Go 的协程调度模型,包括 Goroutine(G)、机器线程(M)和 P(处理器)。调度器根据 G 和 M 的数量动态分配。
- 是的,修改取出的值会影响 map 中的值,前提是通过引用修改。
- map 是基于哈希表实现的,扩容时会创建更大的哈希表并重新分配键值对。
- nil map 不能插入数据,空 map 可以进行插入和读取。
- 使用 sync.Map 或在读写 map 时加锁来处理并发访问。
- 是的,删除 key 后会释放内存。
- map 的循环是无序的,每次遍历的顺序可能不同。
- Go 的 map 不是并发安全的,使用 sync.Mutex 或 sync.Map 来保证安全。
- Channel 基于循环队列和互斥锁实现,支持并发安全的读写。
- 是的,channel 是线程安全的,可以在多个 goroutine 中安全地读写。
- 常用于 API 请求的超时控制、取消信号的传递、请求范围内数据的传递,避免 goroutine 泄露。
- context 是一个携带超时、取消信号、请求范围内数据的上下文,常用于控制 goroutine 的生命周期。
- 闭包是指能够捕获其外部变量的函数,即使外部函数已返回,闭包仍然可以访问这些变量。
- 如果结构体中所有字段都是可比较的(如没有包含 slice、map、function),那么可以直接比较。
- 当对 slice 进行扩展时,底层数组可能会重新分配,导致原 slice 的引用失效。
- 一般来说,对于大结构体,使用指针可以避免拷贝,提高性能;对于小结构体,传值通常足够。
- 反射是通过 reflect 包实现的,可以动态获取类型信息和结构体字段标签(tag)。
- rune 是 Go 的内建类型,表示一个 Unicode 码点,实际上是 int32 的别名,用于表示字符。
- 当 uint 类型的值超过其最大值时,会从 0 开始重新计数,不会引发 panic。
-
-
-
-
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。