跳到主要内容

Go 语言核心面经

Go 语言面试重点

1. Goroutine 与调度器

GMP 模型

G (Goroutine):Go 协程,轻量线程,初始堆 2KB
M (Machine):操作系统线程
P (Processor):调度器,可运行的 goroutine 队列

每个 P 有本地队列 + 全局队列
M 必须持有 P 才能执行 G
G 发生系统调用阻塞时,MP 分离,其他 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 有一小撑开销,循环中大量使用时用匿名函数包裹