无项目 Go 面试 Q&A
1. 聊聊你平时怎么学 Go 的?
花了很多时间手撕 Go 官方文档和《Go 语言圣经》。每天用 Go 刷 3 道 LeetCode,所有代码必须通过 go vet 和 -race 检测。
练手项目是用 Gin + GORM + MySQL 写的简易博客后端,规模不大,但按 Uber Go 代码规范编写,所有对外接口都写了表驱动单元测试(Table-Driven Test),注重可读性和健壮性。
2. make 和 new 到底怎么分?
make 只用于 slice、map、channel 三种类型,返回初始化好的值本身(可直接使用的切片、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,谁负责让它能退出。
- 涉及 goroutine 时使用
context.WithCancel或WithTimeout,父函数退出时defer cancel()。 for-select循环中保留case <-ctx.Done(): return。- 禁止
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. 假如线上内存飙高了,你怎么查?
排查流程:
- 查看
/debug/pprof/heap,定位内存分配最多的函数。 - 查看
/debug/pprof/goroutine,排查chan send阻塞导致无法退出的 Goroutine。 - 用
go tool trace看 GC 频率;若 GC 频繁,说明堆上对象过多,用go build -gcflags="-m"检查逃逸分析,将不必要的指针传参改为值传递。
防御手段:完善日志、超时控制、context 传递、defer recover 兜底。