Skip to main content

无项目 Go 面试 Q&A

1. 聊聊你平时怎么学 Go 的?

花了很多时间手撕 Go 官方文档和《Go 语言圣经》。每天用 Go 刷 3 道 LeetCode,所有代码必须通过 go vet-race 检测。

练手项目是用 Gin + GORM + MySQL 写的简易博客后端,规模不大,但按 Uber Go 代码规范编写,所有对外接口都写了表驱动单元测试(Table-Driven Test),注重可读性和健壮性。


2. makenew 到底怎么分?

make 只用于 slicemapchannel 三种类型,返回初始化好的值本身(可直接使用的切片、map 或 channel)。

new 可用于任意类型,分配内存并置零,返回指针,不做内部结构初始化。

new 创建 slice 得到的是 *[]int,底层数组为空,无法直接 append,必须用 make


3. Slice 扩容到底怎么扩的?

原容量小于 1024 时,新容量翻倍(×2);原容量大于等于 1024 时,新容量按 1.25 倍增长(×1.25),直到满足所需容量。

扩容后若原底层数组空间足够则复用;否则迁移到新内存块。因此 append 后必须接收返回值。


4. defer 遇上 return,到底是先哪个?

return 分两步:① 给返回值赋值 → ② 执行 RET 跳转。defer 在第①步和第②步之间执行。

匿名返回值(func f() int):defer 无法修改最终结果,因为步骤①赋值的是拷贝。

有名返回值(func f() (i int)):defer 可以直接修改 i,影响最终返回值。


5. 怎么避免 Goroutine 泄露?

核心原则:谁创建 Goroutine,谁负责让它能退出。

  1. 涉及 goroutine 时使用 context.WithCancelWithTimeout,父函数退出时 defer cancel()
  2. for-select 循环中保留 case <-ctx.Done(): return
  3. 禁止 select{} 空跑和没有退出条件的 for {} 死循环。

6. 接口(Interface)的 nil 坑在哪?

接口是否为 nil 取决于数据指针和类型指针两者:任一有值则接口不为 nil

var p *int = nil
var i interface{} = p
// i == nil 为 false,因为 i 记录了类型 *int

返回 error 时直接 return nil,不要 return (*MyError)(nil),否则调用方 err == nil 判断失效。


7. sync.Map 能不能随便用?

不能。sync.Map 不是 map 的通用并发安全替代品,误用反而更慢。

适用场景:读多写少(如全局缓存,读 100 次写 1 次)。

读写都频繁的场景(如用户 Session)应使用普通 map + sync.RWMutex


8. 手写一个线程安全的单例

package singleton

import "sync"

type MyStruct struct {
Name string
}

var (
instance *MyStruct
once sync.Once
)

func GetInstance() *MyStruct {
once.Do(func() {
instance = &MyStruct{Name: "default"}
})
return instance
}

sync.Once.Do 底层实现双重检查锁(Double-Checked Locking),保证初始化只执行一次,比手写 Mutex 更安全。


9. 假如线上内存飙高了,你怎么查?

排查流程:

  1. 查看 /debug/pprof/heap,定位内存分配最多的函数。
  2. 查看 /debug/pprof/goroutine,排查 chan send 阻塞导致无法退出的 Goroutine。
  3. go tool trace 看 GC 频率;若 GC 频繁,说明堆上对象过多,用 go build -gcflags="-m" 检查逃逸分析,将不必要的指针传参改为值传递。

防御手段:完善日志、超时控制、context 传递、defer recover 兜底。