HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 技术面试完全指南

    • 技术面试完全指南
    • 8年面试官告诉你:90%的简历在第一轮就被刷掉了
    • 刷了500道LeetCode,终于明白大厂算法面试到底考什么
    • 高频算法题精讲-双指针与滑动窗口
    • 03-高频算法题精讲-二分查找与排序
    • 04-高频算法题精讲-树与递归
    • 05-高频算法题精讲-图与拓扑排序
    • 06-高频算法题精讲-动态规划
    • Go面试必问:一道GMP问题,干掉90%的候选人
    • 08-数据库面试高频题
    • 09-分布式系统面试题
    • 10-Kubernetes与云原生面试题
    • 11-系统设计面试方法论
    • 前端面试高频题
    • AI 与机器学习面试题
    • 行为面试与软技能

Go面试必问:一道GMP问题,干掉90%的候选人

开篇

字节跳动Go岗位二面现场。

面试官看着简历上"精通Go并发编程"的描述,抛出第一个问题:

"说说Go的GMP模型。"

候选人流利回答:"G是Goroutine,M是Machine线程,P是Processor处理器..."

面试官打断:"等等,P存在的意义是什么?为什么不直接让M从全局队列取G?"

候选人愣住了。

面试官继续追问:"那你知道Work Stealing机制吗?为什么要从队尾偷一半而不是全部?"

候选人彻底卡壳。


这不是虚构场景。在字节、阿里、美团、B站的Go岗位面试中,90%的候选人倒在GMP的深度追问上。

他们能背出"G-M-P"三个字母的含义,却说不清这套模型解决了什么问题。

本文将从面试官的视角出发,揭示GMP、Channel、GC这三大高频考点的深层逻辑。不只是告诉你怎么答,更告诉你为什么这么问。


本文结构

第一章 GMP调度模型
├── 1.1 面试官的提问层次
├── 1.2 3分钟标准答案
├── 1.3 深度追问全拆解
├── 1.4 常见误区纠正
└── 1.5 让面试官眼前一亮的加分项

第二章 Channel底层实现
├── 2.1 从无缓冲到有缓冲
├── 2.2 底层数据结构hchan
├── 2.3 发送和接收的完整流程
├── 2.4 性能优化实战
└── 2.5 经典面试编程题

第三章 GC机制
├── 3.1 三色标记法详解
├── 3.2 写屏障的必要性
├── 3.3 GC性能分析工具
├── 3.4 优化GC的5个技巧
└── 3.5 生产环境调优案例

第四章 常见坑点与最佳实践
├── 4.1 Goroutine泄漏的3种姿势
├── 4.2 并发map的正确用法
└── 4.3 select的随机性陷阱

第一章 GMP调度模型

1.1 面试官的提问层次

面试官问GMP,通常分三个层次:

初级:"Go的调度模型是什么样的?" → 考察:是否知道基本概念

中级:"为什么Go要设计GMP模型?GM模型有什么问题?" → 考察:是否理解设计动机

高级:"一个Goroutine从创建到执行,经历了哪些步骤?" → 考察:是否读过源码或深入研究过

如果只能答出第一层,说明只是"会用Go";能答出第二层,说明"理解Go";能答出第三层,才算"精通Go"。


1.2 3分钟标准答案

Go的调度器采用GMP模型:

组件全称作用
GGoroutine协程,Go调度的基本单位
MMachine内核线程,真正执行G的载体
PProcessor处理器,M执行G必须绑定P,P的数量决定并行度

为什么需要P?

早期Go只有GM模型,M从全局队列获取G执行。存在三个严重问题:

问题具体表现影响
锁竞争激烈所有M争抢一把全局锁高并发时性能骤降
缓存利用率低M之间频繁切换GCPU缓存频繁失效
G饥饿M阻塞时持有的G无法被执行响应延迟增加

引入P后:

优化点                    效果
─────────────────────────────────────
每个P有本地队列           减少90%的锁竞争
G优先从本地队列获取        缓存命中率提升
M阻塞时P可转移            G不会饥饿

调度流程:

创建G → 放入P本地队列 → M从队列获取G → M执行G → G完成或阻塞 → 获取下一个G

1.3 深度追问全拆解

追问1:P的数量怎么确定?

// 默认值:等于CPU核心数
numP := runtime.NumCPU()

// 查看当前值
fmt.Println(runtime.GOMAXPROCS(0)) // 0表示只查询,不修改

// 手动设置
runtime.GOMAXPROCS(4)

面试官想听的关键点:

场景              最优P值                  原因
─────────────────────────────────────────────────────
CPU密集型         = CPU核心数              超过无意义,切换开销反而增加
IO密集型          > CPU核心数              Goroutine经常阻塞,需要更多P利用CPU
混合型            1.5~2倍核心数            需要根据实际情况调整

实验验证:

package main

import (
    "fmt"
    "runtime"
    "sync"
    "time"
)

func main() {
    // 测试不同GOMAXPROCS对CPU密集型任务的影响
    for _, p := range []int{1, 2, 4, 8, 16} {
        runtime.GOMAXPROCS(p)

        start := time.Now()
        var wg sync.WaitGroup

        for i := 0; i < 1000; i++ {
            wg.Add(1)
            go func() {
                defer wg.Done()
                sum := 0
                for j := 0; j < 1000000; j++ {
                    sum += j
                }
            }()
        }

        wg.Wait()
        fmt.Printf("GOMAXPROCS=%d, 耗时:%v\n", p, time.Since(start))
    }
}

// 4核CPU输出示例:
// GOMAXPROCS=1,  耗时:5.2s
// GOMAXPROCS=2,  耗时:2.6s
// GOMAXPROCS=4,  耗时:1.3s
// GOMAXPROCS=8,  耗时:1.3s   ← 超过核心数无提升
// GOMAXPROCS=16, 耗时:1.4s   ← 切换开销反而增加

追问2:M的数量怎么确定?

M的数量是动态的:

属性值说明
最小值1至少要有一个M执行G
最大值10000硬编码在runtime中
默认值动态根据需要创建,不超过10000

什么时候创建新M?

场景1:所有M都在执行G,且有空闲的P
       → 创建新M来利用这个P

场景2:M发生系统调用(阻塞)
       → P与M解绑,寻找空闲M或创建新M

源码层面(go1.21 runtime/proc.go):

func startm(_p_ *p, spinning bool) {
    // 尝试获取空闲的M
    mp := acquirem()
    if mp == nil {
        // 没有空闲M,创建新M
        newm(fn, _p_)
    }
}

追问3:Work Stealing机制

定义:当P的本地队列为空时,M会尝试"偷"其他P的G来执行。

偷取顺序:

1. 从全局队列获取G
2. 从其他P的本地队列偷取一半G(从队尾偷)
3. 从网络轮询器获取G

为什么从队尾偷一半?

这是面试官最爱问的细节:

策略原因
从队尾偷队尾的G是最新加入的,缓存热度高
偷一半避免频繁窃取,减少P之间的竞争

观察Work Stealing:

GODEBUG=schedtrace=1000 go run main.go

# 输出示例:
# SCHED 0ms: gomaxprocs=4 idleprocs=0 threads=5 ...
# SCHED 1000ms: gomaxprocs=4 idleprocs=2 threads=5 ...
#
# gomaxprocs: P的数量
# idleprocs: 空闲P的数量(这些P的G被偷走了)
# threads: M的数量

1.4 常见误区纠正

误区正确理解
"Goroutine是绿色线程"Goroutine是协程,切换成本比线程低100倍(只保存/恢复寄存器,不涉及内核态切换)
"GOMAXPROCS越大越好"超过CPU核心数对CPU密集型任务无提升,反而增加切换开销
"一个Goroutine对应一个线程"多个Goroutine共享少量线程,这是Go高并发的核心
"Goroutine没有栈"每个Goroutine有独立的栈,初始2KB,可动态增长到1GB

1.5 让面试官眼前一亮的加分项

加分项1:抢占式调度的演进

Go 1.13及之前:协作式调度
    问题:如果Goroutine不主动让出CPU(如无函数调用的死循环),会一直占用

Go 1.14开始:基于信号的抢占式调度
    解决:sysmon线程定期发送SIGURG信号,强制抢占长时间运行的Goroutine
    效果:单个Goroutine最多运行10ms就会被抢占

加分项2:sysmon监控线程

sysmon是一个特殊的M,不需要绑定P,专门负责:
├── 抢占长时间运行的G(超过10ms)
├── 回收长时间空闲的P
├── 触发GC
└── 网络IO轮询(netpoll)

加分项3:网络IO的优化

普通模型:网络IO阻塞时,整个线程阻塞
Go模型:  网络IO使用非阻塞+epoll/kqueue
          ├── Goroutine尝试读写
          ├── 数据未就绪时,G挂到netpoller,M继续执行其他G
          └── 数据就绪时,netpoller唤醒G

效果:一个M可以服务数千个网络连接

第二章 Channel底层实现

2.1 从无缓冲到有缓冲

// 无缓冲Channel:发送和接收必须同时就绪(同步)
ch := make(chan int)

// 有缓冲Channel:缓冲区未满时发送不阻塞,未空时接收不阻塞(异步)
ch := make(chan int, 10)

何时使用哪种?

类型使用场景语义
无缓冲Goroutine同步、信号通知"我发了你必须收"
有缓冲生产者-消费者模式、削峰填谷"我发了先存着"

2.2 底层数据结构hchan

type hchan struct {
    qcount   uint           // 当前队列中的元素个数
    dataqsiz uint           // 环形队列大小(缓冲区容量)
    buf      unsafe.Pointer // 环形队列指针
    elemsize uint16         // 元素大小
    closed   uint32         // 是否关闭
    sendx    uint           // 发送索引(下一个写入位置)
    recvx    uint           // 接收索引(下一个读取位置)
    recvq    waitq          // 接收等待队列(阻塞的接收者)
    sendq    waitq          // 发送等待队列(阻塞的发送者)
    lock     mutex          // 互斥锁
}

图解:

            ┌─────────────────────────────────────┐
            │              hchan                  │
            ├─────────────────────────────────────┤
            │  qcount=3    dataqsiz=5            │
            │  closed=0    elemsize=8            │
            ├─────────────────────────────────────┤
            │          环形缓冲区 buf              │
            │  ┌───┬───┬───┬───┬───┐            │
            │  │ 1 │ 2 │ 3 │   │   │            │
            │  └───┴───┴───┴───┴───┘            │
            │      ↑           ↑                 │
            │    recvx       sendx               │
            ├─────────────────────────────────────┤
            │  recvq: [G1, G2]  (等待接收的G)    │
            │  sendq: []        (等待发送的G)    │
            └─────────────────────────────────────┘

2.3 发送和接收的完整流程

发送流程 ch <- v:

       ┌──────────────────────────────────────────┐
       │                  加锁                     │
       └──────────────────┬───────────────────────┘
                          ▼
                ┌─────────────────────┐
                │  有等待的接收者吗?  │
                └────┬───────────┬────┘
                     │是         │否
                     ▼           ▼
              ┌────────────┐  ┌─────────────────────┐
              │直接发给它  │  │   缓冲区有空间吗?   │
              │(唤醒接收G)│  └────┬──────────┬────┘
              └────────────┘       │是         │否
                                   ▼           ▼
                            ┌────────────┐ ┌─────────────┐
                            │放入缓冲区  │ │加入sendq    │
                            └────────────┘ │当前G阻塞    │
                                           └─────────────┘
                                    ▼
                          ┌──────────────────┐
                          │       解锁        │
                          └──────────────────┘

接收流程 v := <-ch:

       ┌──────────────────────────────────────────┐
       │                  加锁                     │
       └──────────────────┬───────────────────────┘
                          ▼
                ┌─────────────────────┐
                │  有等待的发送者吗?  │
                └────┬───────────┬────┘
                     │是         │否
                     ▼           ▼
              ┌────────────┐  ┌─────────────────────┐
              │直接从它接收│  │   缓冲区有数据吗?   │
              │(唤醒发送G)│  └────┬──────────┬────┘
              └────────────┘       │是         │否
                                   ▼           ▼
                            ┌────────────┐ ┌─────────────┐
                            │从缓冲区取  │ │加入recvq    │
                            └────────────┘ │当前G阻塞    │
                                           └─────────────┘
                                    ▼
                          ┌──────────────────┐
                          │       解锁        │
                          └──────────────────┘

2.4 关闭Channel的规则

ch := make(chan int, 2)
ch <- 1
ch <- 2
close(ch)

// ✅ 可以继续接收
v1 := <-ch // v1 = 1
v2 := <-ch // v2 = 2

// ⚠️ 再接收返回零值和false
v3, ok := <-ch // v3 = 0, ok = false

// ❌ 向关闭的Channel发送 → panic
ch <- 3 // panic: send on closed channel

// ❌ 重复关闭 → panic
close(ch) // panic: close of closed channel

最佳实践:

// 规则1:只由发送方关闭Channel
// 规则2:使用range遍历,自动检测关闭
for v := range ch {
    process(v)
}

// 规则3:接收时检查是否关闭
v, ok := <-ch
if !ok {
    fmt.Println("Channel已关闭")
}

2.5 性能优化实战

技巧1:选择合适的缓冲区大小

// 性能对比实验
func benchmarkChannel(bufSize int) {
    ch := make(chan int, bufSize)
    start := time.Now()

    go func() {
        for i := 0; i < 1000000; i++ {
            ch <- i
        }
        close(ch)
    }()

    for range ch {}

    fmt.Printf("缓冲区%d, 耗时:%v\n", bufSize, time.Since(start))
}

// 结果:
// 缓冲区0,     耗时:150ms
// 缓冲区1,     耗时:120ms
// 缓冲区100,   耗时:80ms
// 缓冲区1000,  耗时:75ms
// 缓冲区10000, 耗时:75ms  ← 收益递减

结论:缓冲区大小应根据生产消费速率差异设置,通常100-1000足够。


技巧2:避免Goroutine泄漏

// ❌ 错误:发送方永远阻塞
func leak() {
    ch := make(chan int)
    go func() {
        ch <- 1 // 没有接收者,永远阻塞,Goroutine泄漏
    }()
}

// ✅ 正确:使用超时或Context
func noLeak() {
    ch := make(chan int)
    go func() {
        select {
        case ch <- 1:
            // 发送成功
        case <-time.After(1 * time.Second):
            // 超时退出
        }
    }()
}

// 检测泄漏的测试方法
func TestLeak(t *testing.T) {
    before := runtime.NumGoroutine()

    leak() // 调用可能泄漏的函数

    time.Sleep(1 * time.Second)
    after := runtime.NumGoroutine()

    if after > before {
        t.Errorf("Goroutine泄漏: before=%d, after=%d", before, after)
    }
}

2.6 经典面试编程题

题目1:实现支持超时的Channel读取

func readWithTimeout(ch <-chan int, timeout time.Duration) (int, error) {
    select {
    case v := <-ch:
        return v, nil
    case <-time.After(timeout):
        return 0, errors.New("timeout")
    }
}

题目2:用Channel实现互斥锁

type Mutex struct {
    ch chan struct{}
}

func NewMutex() *Mutex {
    m := &Mutex{ch: make(chan struct{}, 1)}
    m.ch <- struct{}{} // 初始状态:未锁定(有令牌)
    return m
}

func (m *Mutex) Lock() {
    <-m.ch // 获取令牌(阻塞直到可用)
}

func (m *Mutex) Unlock() {
    m.ch <- struct{}{} // 释放令牌
}

题目3:用Channel实现信号量

type Semaphore struct {
    ch chan struct{}
}

func NewSemaphore(n int) *Semaphore {
    s := &Semaphore{ch: make(chan struct{}, n)}
    for i := 0; i < n; i++ {
        s.ch <- struct{}{}
    }
    return s
}

func (s *Semaphore) Acquire() {
    <-s.ch
}

func (s *Semaphore) Release() {
    s.ch <- struct{}{}
}

// 使用示例:限制并发数为10
sem := NewSemaphore(10)
for i := 0; i < 100; i++ {
    go func() {
        sem.Acquire()
        defer sem.Release()
        doWork()
    }()
}

第三章 GC机制

3.1 三色标记法详解

Go使用并发三色标记清除算法:

三色定义:

颜色含义最终命运
白色未被标记的对象被清除
灰色已标记,但引用的对象还未扫描待处理
黑色已标记,引用的对象也已扫描保留

标记流程:

       初始状态:所有对象都是白色
                   │
                   ▼
       从根对象(全局变量、栈)开始,标记为灰色
                   │
                   ▼
       ┌──────────────────────────────────┐
       │  从灰色队列取出对象              │◄──┐
       │  ├── 标记为黑色                  │   │
       │  └── 将其引用的白色对象标记为灰色 │   │
       └───────────────┬──────────────────┘   │
                       │                      │
                       ▼                      │
                 还有灰色对象?  ───是────────┘
                       │
                       │否
                       ▼
                 清除所有白色对象

图解示例:

阶段1:初始状态(全白)
┌───┐   ┌───┐   ┌───┐
│ A │──▶│ B │──▶│ C │
└───┘   └───┘   └───┘
  ○       ○       ○      ○ = 白色

阶段2:A是根对象,标记为灰色
┌───┐   ┌───┐   ┌───┐
│ A │──▶│ B │──▶│ C │
└───┘   └───┘   └───┘
  ◐       ○       ○      ◐ = 灰色

阶段3:处理A,A变黑,B变灰
┌───┐   ┌───┐   ┌───┐
│ A │──▶│ B │──▶│ C │
└───┘   └───┘   └───┘
  ●       ◐       ○      ● = 黑色

阶段4:处理B,B变黑,C变灰
┌───┐   ┌───┐   ┌───┐
│ A │──▶│ B │──▶│ C │
└───┘   └───┘   └───┘
  ●       ●       ◐

阶段5:处理C,C变黑,没有灰色了
┌───┐   ┌───┐   ┌───┐
│ A │──▶│ B │──▶│ C │
└───┘   └───┘   └───┘
  ●       ●       ●

阶段6:清除所有白色对象(本例中没有)

3.2 写屏障的必要性

问题场景:并发标记时,应用程序修改了引用关系

初始状态:
A(黑色) ──▶ B(白色)
C(灰色) ──▶ D(白色)

应用程序执行:
A.field = D  // A现在引用D
C.field = nil // C不再引用D

如果没有写屏障:
├── A已经是黑色,不会再扫描
├── D不会被标记(因为A不会再处理)
├── D是白色,会被误删!
└── 程序崩溃:访问已释放的内存

写屏障的作用:

// Go的混合写屏障(Go 1.8引入)
writePointer(slot, ptr):
    shade(ptr)       // 标记新值为灰色(插入写屏障)
    shade(*slot)     // 标记旧值为灰色(删除写屏障)
    *slot = ptr      // 执行实际的写操作

3.3 GC性能分析工具

方法1:GODEBUG环境变量

GODEBUG=gctrace=1 go run main.go

# 输出解读:
# gc 1 @0.005s 0%: 0.018+1.3+0.003 ms clock, 0.14+0.35/1.2/3.0+0.027 ms cpu, 4->4->3 MB, 5 MB goal, 8 P
#
# gc 1        : 第1次GC
# @0.005s     : 程序运行0.005秒时触发
# 0%          : GC占用CPU时间的百分比
# 0.018+1.3+0.003 ms : STW时间 + 并发标记时间 + STW时间
# 4->4->3 MB  : GC前堆大小 -> GC后堆大小 -> 存活对象大小
# 5 MB goal   : 下次GC的目标堆大小
# 8 P         : P的数量

方法2:runtime.ReadMemStats

var m runtime.MemStats
runtime.ReadMemStats(&m)

fmt.Printf("当前堆内存: %v MB\n", m.Alloc / 1024 / 1024)
fmt.Printf("历史分配总量: %v MB\n", m.TotalAlloc / 1024 / 1024)
fmt.Printf("系统内存: %v MB\n", m.Sys / 1024 / 1024)
fmt.Printf("GC次数: %v\n", m.NumGC)
fmt.Printf("GC总暂停时间: %v ms\n", m.PauseTotalNs / 1000000)

方法3:pprof可视化

import _ "net/http/pprof"

go func() {
    http.ListenAndServe("localhost:6060", nil)
}()

// 访问 http://localhost:6060/debug/pprof/heap
// 或命令行分析:
// go tool pprof http://localhost:6060/debug/pprof/heap

3.4 优化GC的5个技巧

技巧1:减少对象分配

// ❌ 每次循环都分配新对象
for i := 0; i < 1000000; i++ {
    s := fmt.Sprintf("hello%d", i) // 分配string
    process(s)
}

// ✅ 复用buffer
var buf bytes.Buffer
for i := 0; i < 1000000; i++ {
    buf.Reset()
    buf.WriteString("hello")
    buf.WriteString(strconv.Itoa(i))
    process(buf.String())
}

技巧2:使用sync.Pool

var bufPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func process() {
    buf := bufPool.Get().(*bytes.Buffer)
    defer bufPool.Put(buf)

    buf.Reset()
    buf.WriteString("hello")
    // ...
}

技巧3:预分配切片容量

// ❌ 频繁扩容
var results []Result
for _, item := range items {
    results = append(results, process(item))
}

// ✅ 预分配容量
results := make([]Result, 0, len(items))
for _, item := range items {
    results = append(results, process(item))
}

技巧4:避免大对象分配在堆上

// 查看逃逸分析
// go build -gcflags="-m" main.go

// ❌ 返回局部变量指针,逃逸到堆
func createUser() *User {
    u := User{}     // moved to heap: u
    return &u
}

// ✅ 传入指针参数,避免逃逸
func initUser(u *User) {
    u.Name = "test"
}

技巧5:调整GC参数

// 设置GC触发的堆增长比例
// 默认100(即堆翻倍时触发GC)
// 设置为200表示堆增长到3倍时才触发
debug.SetGCPercent(200)

// 权衡:减少GC频率,但增加内存占用

3.5 生产环境调优案例

场景:API服务QPS 10000,P99延迟经常飙高

排查过程:

# 1. 查看GC频率
GODEBUG=gctrace=1 ./server

# 发现:每秒GC 5次,每次STW 10ms

# 2. 查看内存分配热点
go tool pprof http://localhost:6060/debug/pprof/allocs

# 发现:大量JSON序列化和字符串拼接

优化措施:

// 1. 使用sync.Pool复用请求对象
var reqPool = sync.Pool{
    New: func() interface{} {
        return &Request{}
    },
}

// 2. 使用strings.Builder替代+拼接
var builder strings.Builder
builder.WriteString("hello")
builder.WriteString("world")

// 3. 预分配切片容量
results := make([]Result, 0, 100)

// 4. 调大GC百分比
debug.SetGCPercent(200)

优化效果:

指标优化前优化后
GC频率5次/秒1次/秒
P99延迟50ms10ms
内存占用500MB800MB

第四章 常见坑点与最佳实践

4.1 Goroutine泄漏的3种姿势

// 坑1:无缓冲Channel,发送方永远阻塞
func leak1() {
    ch := make(chan int)
    go func() {
        ch <- 1 // 永远阻塞,因为没有接收方
    }()
}

// 坑2:有缓冲Channel,缓冲区满
func leak2() {
    ch := make(chan int, 1)
    ch <- 1
    go func() {
        ch <- 2 // 永远阻塞,因为缓冲区已满
    }()
}

// 坑3:没有退出条件的死循环
func leak3() {
    go func() {
        for {
            // 忘记添加退出条件
            doSomething()
        }
    }()
}

修复方案:使用Context控制生命周期

func noLeak(ctx context.Context) {
    go func() {
        for {
            select {
            case <-ctx.Done():
                return // 收到取消信号,退出
            default:
                doSomething()
            }
        }
    }()
}

4.2 并发map的正确用法

// ❌ 并发读写map会panic
m := make(map[int]int)
go func() { m[1] = 1 }()
go func() { m[2] = 2 }()
// fatal error: concurrent map writes

// ✅ 方案1:sync.Map(适合读多写少)
var sm sync.Map
sm.Store(1, 1)
sm.Store(2, 2)
v, ok := sm.Load(1)

// ✅ 方案2:加锁(适合写多的场景)
var mu sync.RWMutex
var m = make(map[int]int)

// 写
mu.Lock()
m[1] = 1
mu.Unlock()

// 读
mu.RLock()
v := m[1]
mu.RUnlock()

选择标准:

场景推荐方案原因
读多写少sync.Map读无锁,性能好
写多RWMutex + mapsync.Map写性能较差
简单场景Mutex + map代码简单,不易出错

4.3 select的随机性陷阱

ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
ch1 <- 1
ch2 <- 2

select {
case v := <-ch1:
    fmt.Println("ch1:", v)
case v := <-ch2:
    fmt.Println("ch2:", v)
}

// ⚠️ 输出是随机的!可能是ch1,也可能是ch2

需要优先级时的正确写法:

// 方案1:嵌套select
select {
case v := <-highPriority:
    process(v)
default:
    select {
    case v := <-highPriority:
        process(v)
    case v := <-lowPriority:
        process(v)
    }
}

// 方案2:分开检查
if len(highPriority) > 0 {
    v := <-highPriority
    process(v)
} else {
    select {
    case v := <-highPriority:
        process(v)
    case v := <-lowPriority:
        process(v)
    }
}

本章小结

Go语言面试的三大核心考点:

考点核心要点面试官关注
GMPP的引入解决了什么问题理解设计动机,不是死记硬背
Channel底层hchan结构发送接收流程,性能优化
GC三色标记+写屏障如何分析和优化GC性能

面试回答技巧:

  1. 先答框架:用30秒给出标准答案
  2. 主动深入:不等追问,主动说出关键细节
  3. 结合实践:举出实际遇到的问题和解决方案
  4. 承认边界:不知道的坦诚说不知道,比瞎编强

下期预告

下一篇:《MySQL面试经典:为什么用B+树而不是B树?99%的人答不到点上》

将揭秘:

  • 面试官问索引时,到底想考察什么
  • B树 vs B+树的本质区别
  • 覆盖索引、回表、索引下推的深层原理
  • SQL优化的系统方法论

💬 互动话题

评论区聊聊:在Go面试中,你遇到过哪些刁钻的问题?

Prev
06-高频算法题精讲-动态规划
Next
08-数据库面试高频题