学习目标
学完本章你应该能够:
- 独立手写 Channel 相关题(交替打印、有界队列、fan-in/out、超时、Semaphore、素数筛、goroutine 链),并解释底层:hchan 结构、缓冲/无缓冲收发、select 随机、nil channel 行为。
- 讲清 Goroutine 管理与 GMP 调度:Worker Pool、同步原语手写、Context 取消链、Graceful Shutdown,以及 G 阻塞在系统调用/channel 时 M/P 的行为、GOMAXPROCS 与 automaxprocs。
- 手写常见锁与并发原语(Ticket Lock、RWMutex、自旋锁、sync.Once、CAS 无锁、分段锁),并说清 sync 包源码(Mutex 饥饿、WaitGroup、Map)。
- 手写核心数据结构(动态数组/链表/哈希/LRU/LFU/TTL、跳表/Trie/Bloom/一致性哈希/线段树/ZSet、RingBuffer)与经典并发问题(生产者消费者、哲学家、Singleflight)。
- 讲清网络协议与系统实现(TCP Echo、粘包拆包、TIME_WAIT、epoll、HTTPS、gRPC 流)及性能调优(pprof / trace / race / 逃逸分析 / GOGC)。
前置知识:
- Go 基础(goroutine / channel / 锁 / context)
- 数据结构与算法基础
- GMP 调度概念与 TCP/IP 网络基础
本章你会动手做的事:
- 用两个 channel 交替打印奇偶数 1..100,并解释 select 如何避免死锁。
- 手写 LRU 缓存(map + 双向链表),对比并发安全版"整锁"与"分段锁"的锁粒度取舍。
- 用
errgroup并发抓取多个 URL,任一失败立即取消其余并返回首个错误。
本文档针对西安市场(腾讯瑞驰、华为、中兴、字节等研发中心)面试特点整理,重点覆盖并发编程手撕、数据结构实现、源码原理追问和项目场景问答。
已有基础算法题见:24-算法与手撕代码 、25-算法进阶手撕代码
一、Channel 手撕题(西安市场高频!)
西安面试官从项目细节一路追问到底层原理是常见套路。以下题目必须能手写并能解释每个细节。
1.1 Channel 基础操作
- 用两个 channel 交替打印奇偶数(1,2,3,4…)—— 腾讯瑞驰面试真题
- 用 channel 实现一个有界队列(Bounded Queue),支持超时获取
- 实现 fan-in 模式:多个 channel 合并为一个
- 实现 fan-out 模式:多个 goroutine 消费同一个 channel
- 手写 timeout 函数:
func Timeout(d time.Duration, fn func()) bool - 用 select 实现带超时的 channel 读取
- 实现一个 Semaphore(信号量),控制并发数
- 用 goroutine 链(daisy-chain pipeline)实现素数筛 —— 经典 channel 手撕题
- 只用 channel(不用锁)实现一个并发安全计数器
- 实现任务分发器:master 拆分任务发给 N 个 worker,全部完成后聚合结果
- 用 reflect.Select 同时监听不确定数量的 channel(进阶)
1.2 Channel 底层原理
- 画出 channel 的内存结构图(hchan 结构体)
- 无缓冲 channel 和有缓冲 channel 的 send/receive 流程图
- select 语句的实现原理(随机选择机制)
- nil channel 的行为:发送和接收都会永久阻塞?
- 关闭已关闭的 channel 会发生什么?
- channel 的 hchan 为什么必须带一把锁?保护的是哪些字段?
- 元素在 channel 里是怎么拷贝的?直接传指针有什么风险?
- buffered channel 的容量怎么定?结合批量任务场景给出压测方法
select 多路复用随机选择执行流程图
看图要点:select 不是"从上到下"选第一个就绪的,而是运行时用
fastrand随机打乱 case 顺序后再轮询,命中第一个就绪的;多个就绪时随机选一个以避免饥饿;没有就绪且没有default时,当前 goroutine 被gopark挂起并加入各 case 对应 channel 的等待队列,等某个 channel 就绪后被唤醒、重新加锁执行。面试追问:空 select{} 为什么死锁(永远没有就绪 case、也没有 default,永久 park);nil channel 在 select 里(永远阻塞,可用来动态关闭某分支);为什么 default 不阻塞(default 等价于"无就绪就立即走"的兜底分支)。
1.3 Channel 综合应用
- 实现一个带缓冲的 channel 池
- select 如何实现优先级控制?
- 用 channel 实现一个简易的消息广播系统
- 实现一个基于 channel 的 RPC 框架骨架
- 实现 fan-in 结果收集器:结果与错误分别走两个 channel,聚合后统一返回
- select 多路复用等待「最快返回」的 goroutine(first-win 模式)
二、Goroutine 管理与调度
2.1 Worker Pool(工作池)
- 实现一个基础的 Worker Pool:支持提交任务和限制并发数
- 实现带优雅停止的 Worker Pool(等待任务完成)
- 实现带优先级任务的 Worker Pool
- 对比 ants/pond 等开源库,说出自己的实现差异
- 用 errgroup 并发抓取多个 URL:任一失败立即取消其余,并返回首个错误 —— 字节/腾讯高频
- Worker Pool 的任务队列满了怎么办?阻塞 / 丢弃 / 报错 / 动态扩容的 trade-off
- 实现 ParallelFor(n int, fn func(i int)):并行处理 n 个下标,并 recover 子任务 panic
2.2 同步原语实现
- 手写 WaitGroup 的简化版本(理解原理即可)
- 实现一个 CountDownLatch(倒计时栅栏)
- 实现一个 Barrier(屏障):所有 goroutine 到达后才继续执行
- 实现一个 CyclicBarrier(循环屏障)
2.3 Context 相关
- 实现 context.WithCancel 的简化版本
- 实现 context.WithTimeout 的简化版本
- context 取消链的传播机制:父 cancel → 子 cancel
- 用 context 实现 goroutine 取消链
- context 的使用陷阱:为什么不能存储到数据库或缓存中?
- context.Value 为什么是逐层链式查找?设计上为什么只放请求级元数据?
- 超时预算怎么层层分配?网关 1s → 服务 800ms → DB 100ms 的透传实践
2.4 Graceful Shutdown
- 实现 HTTP 服务的优雅停机(监听 SIGTERM,等待请求处理完毕)
- 实现一个带超时的 graceful shutdown
- 检测 goroutine 泄漏的工具函数
- 平滑重启不丢请求:继承监听 fd 的思路(SO_REUSEPORT / fd 传递)
- 用 goleak 在单元测试里自动检测 goroutine 泄漏
2.5 GMP 调度场景追问(西安面试官最爱深挖)
- 进程、线程、goroutine 三者的区别与联系(资源占用、调度方、切换成本)
- G 阻塞在系统调用时,M 和 P 会发生什么?(hand-off 机制)
- G 阻塞在 channel/锁上时,M 会跟着阻塞吗?(gopark 挂起 G,M 继续跑别的 G)
- GOMAXPROCS=1 时两个 goroutine 还能「并发」吗?为什么?
- 容器 CPU limit=2 核、宿主机 128 核:GOMAXPROCS 默认是多少?会引发什么问题?(automaxprocs)
- 10 万 goroutine vs 10 万线程:栈内存(2KB vs 8MB)与上下文切换开销对比
- Go 1.14 前后的抢占调度差异(函数调用点协作检查 vs 基于信号的异步抢占)
三、锁与并发安全
3.1 锁的实现
- 手写 Ticket Lock(票号锁)—— 理解公平锁原理
- 实现一个读写锁(Readers-Writers Lock),支持读者优先或写者优先
- 用 atomic 实现一个无锁计数器(CompareAndSwap 循环)
- 手写 Condition Variable(条件变量)
- 用 RWMutex 实现一个线程安全的 Cache
- 手写自旋锁(Spinlock),说明什么场景自旋优于阻塞
- 手写 sync.Once:CAS + 双重检查,不许用 sync 包
- 实现分段锁版并发安全 map(Segment Lock),说明与 sync.Map 的适用差异
- 实现并发安全的 []int(append 不丢数据):加锁 / 原子索引 / channel 三种方案对比
3.2 sync 包源码
- sync.Mutex 的实现(含饥饿模式切换)
- sync.RWMutex 的实现(读锁批量释放)
- sync.WaitGroup 的实现(原子操作计数)
- sync.Map 的实现(read map + dirty map + misses 计数)
- sync.Pool 的简化实现(理解对象复用原理)
3.3 经典并发问题
- 生产者 - 消费者模型(多生产多消费,channel 版本)
- 读者 - 写者问题(两种策略:优先读者 / 优先写者)
- 哲学家进餐问题(死锁预防方案)
- Pipeline 模式:多阶段数据处理
- Fork-Join 框架:并行计算并合并结果
- 实现一个 Singleflight(避免重复请求)
- 两个 goroutine 交替打印 1-100:不许用 channel,只用 sync 原语
- 实现 Promise/Future:异步执行 + Get 阻塞取结果 + 支持超时取消
- 手写无锁队列(Michael-Scott 队列简化版,基于 CAS)
3.4 定时器与周期任务(高频手撕方向)
- for-select 循环里反复调用 time.After 会怎样?写出两种修复方式
- time.Ticker 不 Stop 会怎样?defer Stop 的正确位置
- 手写定时任务调度器:支持延时任务 + 周期任务(最小堆或时间轮实现)
- 时间轮(Timing Wheel)的原理与实现 —— Kafka 定时器同款思想
- 手写简易 Cron 解析器(支持
*/5、1-30、0,15,30语法) - 「下单 30 分钟未支付自动取消」:时间轮 / Redis ZSet 轮询 / MQ 延迟队列三方案对比
四、数据结构实现(不只是会用,要会写)
4.1 基础数据结构
- 实现一个动态数组(模仿 Go slice 的扩容机制)
- 底层数组是什么?
- 扩容因子是多少?
- 什么时候触发扩容?
- 实现单链表 + 双向链表
- 实现一个哈希表(处理 hash 冲突:链地址法 / 开放寻址法)
- 实现栈(数组版 + 链表版两种实现)
- 实现队列(数组版 + 环形队列)
- 实现 LRU 缓存(LinkedHashMap,结合 map + 双向链表)
- 实现 LFU 缓存(需额外维护频率计数)
- 实现 TTL 缓存(带过期时间的缓存)
- LRU + TTL 双重淘汰缓存(参考 go-cache / freecache 的设计)
- 并发安全版 LRU:整锁 vs 分段锁的锁粒度设计
4.2 高级数据结构
- 实现跳表(Skip List)—— Redis zset 的底层实现
- 实现 Trie 树(前缀树),用于字符串匹配
- 实现 Bloom Filter(布隆过滤器)—— 判断元素是否存在
- 实现一致性哈希(Consistent Hash Ring)—— 分布式缓存核心
- 实现 Fenwick Tree(二叉索引树)—— 区间求和
- 实现线段树(Segment Tree)—— 区间最值/求和
- 手写 ZSet 简化版:跳表 + map,支持 O(logN) 插入/删除/按排名范围查询
- 实现环形缓冲区 RingBuffer —— 生产者-消费者解耦的底座
4.3 标准库模拟
- 实现一个简化的 container/list
- 手写 heap.Interface 的实现示例
- 实现一个简单的 bufio.Scanner
- 手写 strings.Split / strings.Join
- 手写 strconv.Atoi(含错误处理)与 strconv.Itoa
- 实现内存版 mini-Redis:GET/SET/EXPIRE/DEL,且并发安全
五、设计模式与工程实践
5.1 设计模式
- 单例模式:饿汉式、懒汉式、双重检查(DCL)、sync.Once 对比
- 观察者模式:事件总线 EventEmitter
- 发布订阅模式:带 channel 的版本
- 策略模式:可插拔的算法策略
- 装饰器模式:HTTP 中间件实现
- 责任链模式:Middleware 链
- 适配器模式:接口转换
- Functional Options 模式 —— gRPC/Docker 风格的可选参数 API
- Go 泛型实战:类型参数版 Map/Filter/Reduce 与通用缓存
5.2 并发设计模式
- Rate Limiter(限流器):令牌桶 / 漏桶 / 滑动窗口 三种实现
- Circuit Breaker(熔断器):closed → open → half-open 状态机
- Retry with Exponential Backoff(指数退避重试)
- Bulkhead Pattern(舱壁模式):隔离不同资源池
- Leader Election(领导者选举):基于 etcd/Redis 的简化实现
- Distributed Lock(分布式锁):Redis SET NX + Lua / Etcd 两种方案
- Redis + Lua 实现滑动窗口限流(解决固定窗口的临界突刺)
- 手写雪花算法 Snowflake(重点:时钟回拨怎么处理)
5.3 工程组件
- 实现一个依赖注入容器(DI Container)
- 实现一个 AOP 拦截器(Method Interceptor)
- 实现配置热加载模块(fsnotify + sync.Map)
- 实现日志轮转组件
- 实现一个请求 ID 透传中间件(context 传递)
- 手写一个 HTTP Server:支持路由匹配和中间件链
- 实现一个简易的 RPC 框架(JSON-RPC)
- 手写日志库:分级输出 + 异步刷盘(参考 zap 的设计)
- 慢请求日志中间件:自动记录超阈值请求的耗时明细
- 简易配置中心:监听变更并热推送(watch + 长轮询)
六、网络与协议实现
6.1 TCP/IP
- 手写一个 TCP Echo Server/Client
- 实现一个简单的 HTTP Server(解析请求行和头部)
- 实现一个简单的 HTTP Client(发送请求并解析响应)
- 解析 TCP 报文头(提取源端口、目的端口、序列号等)
- 实现一个简易的 ICMP Ping 工具
- 实现 DNS Resolver(解析域名)
- TCP 粘包/拆包:实现「长度前缀 + 正文」的协议编解码器 —— 中兴网络编程必考
- 单机百万 TCP 连接受什么限制?要调哪些内核参数?
- 应用层心跳包 vs TCP keepalive 的取舍
6.2 HTTP/gRPC
- 实现 HTTP 中间件链(Handler 包装)
- 手写路由匹配器:支持路径参数(如
/users/:id) - 实现 gRPC 的双向流(Bidirectional Streaming)
- 实现 WebSocket Server(握手 + 帧解析)
- 手写 HTTP/2 的流 multiplexing 概念演示
- 实现带连接池的 HTTP Client(MaxIdleConns、连接复用、三级超时)
- 用 httputil.ReverseProxy 实现反向代理,串联限流与熔断中间件
- gRPC 四种通信模式各自的典型业务场景(一元/服务端流/客户端流/双向流)
- 实现 gRPC 拦截器:统一鉴权 + 日志 + recover
- Protobuf 为什么比 JSON 又小又快?(varint + 字段编号 + 二进制布局)
6.3 网络协议追问(中兴/华为西研所常问)
- 三次握手为什么不是两次?SYN Flood 原理与防御(syncookie)
- 四次挥手为什么多一次?TIME_WAIT 为什么要等 2MSL?
- 大量 TIME_WAIT / CLOSE_WAIT 分别暴露什么问题?怎么排查处理?
- TCP 靠哪些机制保证可靠传输?(重传、滑动窗口、流量/拥塞控制)
- select / poll / epoll 区别;epoll 为什么快(红黑树 + 就绪链表)
- HTTP/1.1 → HTTP/2 → HTTP/3:队头阻塞如何一步步解决(QUIC)
- HTTPS 完整握手流程与证书链校验
- 浏览器输入 URL 到页面渲染的全链路(DNS → TCP → TLS → HTTP → 渲染)
- 多级代理后如何拿到客户端真实 IP?(X-Forwarded-For 的伪造问题)
七、存储系统实现
7.1 数据库相关
- 实现一个简单的 SQL 查询解析器(SELECT 语句)
- 手写 B+ 树的节点结构和插入删除逻辑
- 实现 MVCC 的简化版(多版本并发控制)
- 手写分库分表的路由逻辑(一致性哈希 + 取模)
- 实现一个简单的 ORM 映射器(struct ↔ table)
- 手写连接池(Connection Pool):管理数据库连接
- 连接池保活:ping 健康检查 + 最大空闲时长回收
- 深分页优化:LIMIT 100000,20 为什么慢?游标分页 / 延迟关联两种解法
- 批量写入优化:事务合并、批量 INSERT、部分失败怎么办
- EXPLAIN 执行计划:type、key、rows、Extra 分别看什么
- 索引失效的常见写法(函数包裹、隐式类型转换、破坏最左前缀)
- 一条慢 SQL 的完整排查链路:slow log → EXPLAIN → 改写/加索引 → 验证
7.2 缓存与序列化
- 实现一个简单的 Redis Client(解析 RESP 协议)
- 手写 JSON 序列化器(Marshal)
- 手写 JSON 反序列化器(Unmarshal)
- 实现 Protobuf 的 Varint 编码/解码
- 实现缓存预热策略(定期刷新热点数据)
- 手写 LSM Tree 的核心逻辑(MemTable + SSTable)
- 实现 Redis Pipeline 客户端(批量命令一次网络往返)
- 实现 Redis Pub/Sub 的发布端与订阅端
- 序列化选型压测:JSON vs Protobuf vs MsgPack 的体积与编解码耗时
八、性能分析与调试
8.1 Profiling 实践
- 使用 pprof 定位 CPU 热点(命令行 + Web UI)
- 使用 trace 分析 goroutine 调度(阻塞原因)
- 定位内存泄漏的工具链(pprof heap + diff)
- 分析 GC 停顿原因(GOGC 调优)
- 使用 race detector 检测数据竞争
- 手写一个 perf 采样分析器(简化版)
- 线上 CPU 100% 的完整排查路径:top → pprof → 火焰图定位热点函数
- 线上 OOM 排查:heap diff 找增长最快的对象与引用链
- goroutine 数暴涨:用 goroutine profile 分析阻塞点
- 锁竞争与死锁:mutex profile / block profile 的用法
- 如何证明优化有效:压测 before/after + benchstat 对比
8.2 性能优化技巧
- 对象池的使用场景:sync.Pool vs 自定义 pool
- 字节数组 vs string 的性能对比实验
- 减少内存分配的技巧:预分配、复用
- CPU Cache 友好的数据结构设计
- 无锁数据结构:CAS 循环、Michael-Scott 队列
- 使用 atomic 替代 Mutex 的场景分析
- 容器内存 limit 场景:GOGC / GOMEMLIMIT 调优
- 高频小对象场景怎么降低 GC 压力(对象池、预分配、减少逃逸)
- string ↔ []byte 零拷贝转换:unsafe.Pointer 的前提与风险
- 逃逸分析实战:给一段代码判断哪些变量逃逸(
go build -gcflags=-m验证)
九、源码阅读与原理追问
9.1 Runtime 源码(能说出关键实现)
- GMP 调度器的工作原理(M/P/G 的关系、工作窃取)
- Channel 的底层实现(缓冲区、等待队列、select 机制)
- Map 的底层实现(buckets 结构、hash 计算、扩容触发条件)
- GC 的三色标记法(写屏障:white/black/gray 的定义)
- Interface 的底层结构(itab:type + data)
- defer 的实现原理(deferred function 的栈管理)
- panic/recover 的执行流程(展开栈帧)
- 内联优化的条件和过程(小函数优化)
- 逃逸分析的规则(哪些情况会逃逸到堆)
- slice 扩容规则:Go 1.18 前后的差异(阈值 1024 → 256,平滑增长公式)
- map 两种扩容:翻倍扩容(负载因子 6.5)与等量扩容(溢出桶过多)
- map 删除元素后内存会立即释放吗?怎么彻底释放一个 map?
- 内存分配三级结构:mcache / mcentral / mheap,Tiny/Small/Large 对象划分
- goroutine 栈的扩容与缩容(morestack 与栈整体搬移)
- netpoller:runtime 如何把 epoll 包装成「阻塞式」网络 IO
9.2 标准库源码
- sync.Mutex 的实现(含饥饿模式切换)
- sync.RWMutex 的实现(读锁批量释放)
- sync.WaitGroup 的实现(原子操作计数)
- sync.Map 的实现(read map + dirty map + misses 计数)
- context 包的实现原理(cancel 传播链)
- json.Marshal/Unmarshal 的反射实现
- net/http 的服务端实现(conn 管理、请求分发)
- time.Timer / Ticker 的底层:runtime 定时器四叉堆
- sync.Once 底层:Done 先 CAS、后加锁的 double-check 细节
- atomic 的底层依赖:LOCK 前缀指令与 CPU 缓存一致性(MESI)
- strings.Builder 为什么比 += 拼接快?
十、综合实战题(模拟面试完整项目)
10.1 完整系统实现
- 实现一个简易的 Web Framework(类似 Gin,支持路由、中间件、JSON 响应)
- 实现 gRPC 客户端和服务端的骨架代码
- 实现分布式任务调度系统(带重试和失败告警)
- 实现一个简易的 K8s Controller(Informer + Workqueue)
- 实现 Service Mesh 的数据面(sidecar 代理)
- 实现消息队列的 Producer/Consumer(带 ACK 机制)
- 实现 Redis Cluster 客户端(slot 计算、故障转移)
- 实现 etcd 的 watch 机制(长轮询 + 事件通知)
- 实现 API 网关雏形:路由转发 + 鉴权 + 限流 + 熔断中间件链
- 实现短链接服务:发号器 + Base62 + 缓存/布隆防穿透
- 实现延迟队列:Redis ZSet 轮询版与时间轮版各写一份
- 实现并发爬虫:worker pool + 布隆去重 + 全站速率限制
10.2 性能压测与调优
- 对一个 HTTP 接口进行压测并优化(wrk/vegeta)
- 测试 channel 在不同 buffer 大小下的性能差异
- 测试 Mutex vs atomic 的性能对比
- 测试 map 的读写性能瓶颈(并发读写 panic)
- 测试 goroutine 的创建开销(vs 线程池)
- 压测 worker pool 的最优池大小:CPU 密集 vs IO 密集任务的经验值
- 对比 Mutex 与分段锁在不同读写比例下的吞吐曲线
十一、易错点与陷阱(代码审查视角)
11.1 并发陷阱
- 闭包捕获循环变量的问题(Go 1.22 前后差异)
- goroutine 泄漏的常见场景(channel 阻塞、select 无限等待)
- channel 未关闭导致的永久阻塞
- mutex 未 unlock 的死锁场景(panic 后未释放)
- context 取消但未传播的问题(子 context 未 cancel)
- defer 在循环中的性能问题(每次循环都注册 defer)
- WaitGroup.Add 写在 goroutine 里导致 Wait 提前返回的经典错误
- 向已关闭 channel 发送 vs 从已关闭 channel 接收,行为分别是什么?
- sync.Mutex / WaitGroup 被值拷贝后失效(go vet 的 copylocks 检查)
- sync.Once 里的函数 panic 了算执行过吗?下次调用还会执行吗?
11.2 内存陷阱
- slice 持有底层数组引用导致内存无法释放
- map 并发读写 panic(runtime: map Iteration with mutable load)
- interface 的空值判断误区(nil interface vs interface containing nil)
- large stack 的分配问题(超过阈值走堆分配)
- 内存对齐导致的空间浪费(struct 字段顺序影响大小)
- append 后两个 slice 共享底层数组,互相覆盖的场景分析
- for range 遍历 map 时的插入/删除安全边界
十二、项目场景问答题(西安大厂项目面必问)
西安这些研发中心(腾讯瑞驰、华为西研所、中兴、诺瓦星云等)项目面的固定套路:从简历项目切入,一层层追问技术选型原因和量化结果。以下问题请对照自己的项目,提前准备好具体数字与案例,空泛回答很容易被戳穿。
12.1 并发与架构场景
- 你项目里哪里用到了 goroutine/channel?并发数是怎么定的?依据是什么?
- 如果流量涨 10 倍,哪些模块会先扛不住?你会怎么改造?
- 有没有遇到过数据竞争或死锁?怎么发现的、怎么解决的?
- 你服务的 QPS/RT 峰值是多少?怎么压测出来的?瓶颈在哪一段?
- 描述一次请求在你系统里的完整流转链路(网关 → 服务 → 缓存/DB)
- 技术栈为什么这么选?面试官提出「换成 XX 行不行」时怎么应对?
12.2 稳定性与线上问题
- 讲一个你真实排查过的线上问题:CPU 飙高 / 内存泄漏 / goroutine 泄漏
- 项目里限流、熔断、降级分别怎么做的?阈值怎么定的?
- 服务发布重启时如何不影响在线请求?滚动/灰度策略是怎样的?
- 讲一次完整的线上事故处理:发现 → 止血 → 定位 → 复盘
- 监控告警体系怎么设计的?(指标选型、告警分级、值班响应)
12.3 存储与缓存场景
- 缓存穿透/击穿/雪崩,你的项目里分别怎么防的?
- 缓存与数据库一致性怎么保证?为什么是「先更新库再删缓存」?
- 讲一个你优化过的慢 SQL:从发现到解决的全过程与前后对比
- 数据量涨了以后怎么分库分表?分片键怎么选?跨片查询怎么办?
- Redis 在你项目里承担了哪些角色?(缓存/锁/排行/计数,会被逐个追问实现)
12.4 消息队列与分布式
- 为什么引入 MQ?怎么保证消息不丢、不重复消费?
- 接口幂等性怎么设计的?(下单/支付场景必问)
- 秒杀/抢购怎么防超卖?库存扣减的完整方案(Redis+Lua / DB 乐观锁)
- 分布式事务怎么落地?最终一致性在你项目里的具体实现?
- 多服务间数据一致性怎么保证?补偿与对账机制怎么做?
12.5 项目深挖开放题
- 介绍你最有挑战的一个项目:背景、难点、方案、量化结果
- 如果重新设计这个项目,你会改哪些地方?为什么当时没这么做?
- 项目里最大的技术取舍是什么?(时间/性能/可维护性之间怎么权衡)
- 你负责模块与他人协作的边界怎么划分?联调出过什么问题?
十三、面试题来源说明
本文档整理的题目主要来源:
- 腾讯瑞驰(西安)一面/二面面经:并发编程 + 底层原理追问
- 华为(西安研究所)面试真题:数据结构实现 + 系统设计
- 中兴通讯西安研发岗笔试题:算法手撕 + 网络协议
- 字节跳动(西安)后端开发面经:Goroutine 管理 + 性能优化
- 牛客网 Go 社招/校招面经汇总
西安市场特点提醒:腾讯瑞驰、华为、中兴这类在西安的研发中心,项目面会从你的项目细节一路追问到底层原理。比如你说用了 channel 控制并发,就会追问 channel 底层、为什么不用锁、有没有考虑过无锁方案。建议针对自己简历上每个项目,把相关知识点准备好具体的数字和案例,空泛回答很容易被戳穿。
学习建议
- 先手撕,再优化:每个题目先不限时写出正确解法,再思考优化空间
- 说原理:写完代码要能解释清楚时间复杂度、空间复杂度、边界条件
- 对比实现:手写实现 vs Go 标准库实现,理解差异和 trade-off
- 联系实际:每个知识点都能找到实际项目中的应用场景
- 持续复盘:把做错的题目记录到错题本,定期回顾
自测题与动手练习
nil channel 在 select 里永远阻塞,可用来"动态关掉"某个 case(把对应 channel 置 nil),是控制 select 分支启停的常用技巧。
默认 GOMAXPROCS 读的是宿主机核数 128,但容器被 limit 到 2 核时会出现严重争抢/节流。automaxprocs 会按 cgroup limit 把它改成 2,避免超配。
并发安全版:整锁实现简单但所有操作串行;分段锁把 key 空间分桶各自加锁,冲突少、吞吐高,但实现更复杂——读多写少且 key 分散时优先分段锁。
Go 1.14 前只在函数调用点做协作抢占,若一段代码无调用点(如纯计算循环)会长期不被抢占、甚至阻塞 STW;改为基于信号的异步抢占后,运行时可强制打断,调度更及时。
大量 TIME_WAIT 通常是"短连接高频主动关闭"导致,暴露连接未复用;排查
ss -ant 看状态分布,优先用连接池/Keep-Alive 复用,而非乱调 tcp_tw_recycle。动手练习(建议真做一遍):
- 用两个 channel 交替打印 1..100,再用
select+time.After给每次打印加超时,体会 nil channel 关闭分支。 - 手写 LRU 缓存并加并发安全版(整锁 / 分段锁各一版),benchmark 对比高并发读写吞吐。
- 用
errgroup并发抓取多个 URL,人为让其中一个失败,验证其余被立即取消并返回首个错误。
本章小结
- 本文档是西安市场专项手撕清单,覆盖 Channel、Goroutine/GMP、锁与并发安全、数据结构实现、设计模式/工程组件、网络协议、存储系统、性能调优、源码原理、综合实战、易错点、项目场景十二大块。
- 面试套路是"从简历项目一路追问到底层原理":会写更要能解释时间/空间复杂度、边界条件与 trade-off,并能联系真实项目数字。
- 高频必会:Channel 底层(hchan/缓冲/select 随机/nil channel)、GMP(hand-off、GOMAXPROCS/automaxprocs、异步抢占)、手写锁与 sync 源码、LRU/跳表/一致性哈希等结构、TCP 粘包/TIME_WAIT/epoll。
- 下一篇可顺着这份清单,把其中任意一块(如 Channel 或 GMP)单独深挖成一篇带源码的专题笔记。