Go 语言核心面经
Go 语言面试重点
1. Goroutine 与调度器
GMP 模型
G (Goroutine):Go 协程,轻量线程,初始堆 2KB
M (Machine):操作系统线程
P (Processor):调度器,可运行的 goroutine 队列
每个 P 有本地队列 + 全局队列
M 必须持有 P 才能执行 G
当 G 发生系统调用阻塞时,M 和 P 分离,其他 M 接管 P
工作窃取:P 的本地队列空时从其他 P 的队列尾部窃取
Goroutine 泄漏
// 常见泄漏场景
// 1. channel 没有接收者
ch := make(chan int)
go func() { ch <- 1 }() // 永远阻塞
// 2. goroutine 等待永远不会满足的条件
go func() {
select {
case <- neverClosedChannel:
}
}()
// 解决:始终将 context 传入
func worker(ctx context.Context, ch <-chan int) {
for {
select {
case v, ok := <-ch:
if !ok { return }
process(v)
case <-ctx.Done(): // 外部取消
return
}
}
}
2. Channel 深入
// channel 类型
// 无缓冲:发送和接收必须同时准备
ch1 := make(chan int)
// 有缓冲:发送在缓冲满之前不阻塞
ch2 := make(chan int, 10)
// select 实现多路合并
func fanIn(cs ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, c := range cs {
wg.Add(1)
go func(c <-chan int) {
defer wg.Done()
for v := range c {
out <- v
}
}(c)
}
go func() { wg.Wait(); close(out) }()
return out
}
// 超时控制
select {
case result := <-ch:
process(result)
case <-time.After(3 * time.Second):
fmt.Println("超时")
}
3. 内存管理与 GC
逃逸分析
// 编译器分析变量是分配在栈还是堆上
// 堆:生命周期超过定义作用域的变量
// 栈:局部变量,不逆逸
func bad() *int { // bad: n 逆逸到堆
func bad() *int {
n := 42 // n 逆逸到堆
return &n
}
func good() int { // good: 返回値
n := 42
return n // n 在栈上
}
// 查看逆逸
go build -gcflags='-m' ./...
GC 三色标记清除
白色:待扫描,可能被回收
N 色:正在扫描
N 色:已扫描,确认存活
STW1 -> 标记根节点 ->
并发标记(与业务 goroutine 并行)->
STW2(保添操作)-> 清除白色
写屏障:并发标记期间,对黑色对象的指针修改会标记
4. 接口与多态
// Go 接口是隐式实现的
type Reader interface {
Read(p []byte) (n int, err error)
}
// 任何有 Read 方法的类型都实现了 Reader
type MyReader struct{}
func (r MyReader) Read(p []byte) (n int, err error) { ... }
// 空接口和类型断言
var i interface{} = "hello"
s, ok := i.(string) // 类型断言
switch v := i.(type) {
case string:
fmt.Println("string:", v)
case int:
fmt.Println("int:", v)
}
// 接口的内部表示:(type, value) 元组
// 注意 nil 接口 != 具有 nil 值的接口
var p *int = nil
var i interface{} = p
fmt.Println(i == nil) // false!(type 不为 nil)
5. 并发模式
// Worker Pool
func workerPool(jobs <-chan Job, results chan<- Result, n int) {
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
go func() { wg.Wait(); close(results) }()
}
// sync.Once 实现单例
type Singleton struct{}
var instance *Singleton
var once sync.Once
func GetInstance() *Singleton {
once.Do(func() { instance = &Singleton{} })
return instance
}
// sync.Pool 对象池
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func process() {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
// 使用 buf
}
6. 资深面试题
- Go 的 GC 会 STW 多久?
- Go 1.14+ STW 最大几百微秒,大部分工作并发执行
- 调控 GOGC 环境变量控制 GC 触发阈值(默认 100%)
- make 和 new 的区别?
new(T)分配 T 类型的内存并返回指针,内存为零值make(T, ...)仅用于 slice/map/channel,返回 T 类型(非指针)
- 为什么 slice 并发不安全?
- slice 扩容时会重新分配底层数组,并发修改可能操作舍弃的旧内存
- 采用
sync.Mutex或 channel 保护
- defer 的执行顺序和性能问题?
- LIFO 顺序执行
- defer 在函数返回时执行,可修改命名返回值
- 每个 defer 有一小撑开销,循环中大量使用时用匿名函数包裹