01佬得物一面
面试题#
Go#
- LRU 的底层结构是什么?
- 更新 LRU 里已有的数据,会涉及哪些动作?
- Go 里 slice 的底层结构是什么?
make([]int, 2)和make([]int, 3, 5)有什么区别?map并发安全吗?- 多协程环境下使用
map怎么写? sync.Map有了解吗?- Go 的协程为什么能轻松支持百万高并发?
- 主协程里有多个子协程一起处理任务,怎么实现优雅退出?
- 主协程觉得子协程执行时间太长,希望提前退出,怎么实现?
- 怎么监控
context的取消或超时消息? - 有 1000 个任务,只用 5 个协程执行,怎么实现?
- Go 里哪些问题可能会导致内存泄露?
计算机网络#
- TCP 三次握手和四次挥手。
- 浏览器输入一个网址后发生什么?
- I/O 多路复用有几种选择?
select、poll、epoll的区别和优点是什么?
Redis#
- Redis 五种数据结构。
- ZSet 底层结构。
- Redis 的 RDB 和 AOF 持久化机制。
- Redis 数据过期和数据淘汰。
算法#
- 有 10000 个整数,找到其中最大的 10 个数,用什么数据结构比较好?
参考答案(AI 生成)#
以下答案由 AI 生成,仅供面试复盘参考。
1. LRU 的底层结构是什么?#
答:LRU 通常用哈希表 + 双向链表实现。哈希表负责按 key O(1) 找到节点,双向链表负责维护访问顺序,链表头部放最近访问的数据,尾部放最久未访问的数据。
2. 更新 LRU 里已有的数据,会涉及哪些动作?#
答:先通过哈希表定位节点,再更新节点 value,然后把该节点从原位置摘下来并移动到链表头部。因为 key 已经存在,容量通常保持不变,也就无需触发淘汰。
3. TCP 三次握手和四次挥手#
答:三次握手是客户端发 SYN,服务端回 SYN+ACK,客户端再回 ACK,用于确认双方收发能力和初始化序列号。四次挥手是主动关闭方发 FIN,被动关闭方回 ACK,被动关闭方处理完剩余数据后发 FIN,主动关闭方回 ACK 并进入 TIME_WAIT。
4. 浏览器输入一个网址后发生什么?#
答:浏览器先解析 URL,再查缓存和做 DNS 解析,拿到 IP 后建立 TCP 连接;HTTPS 还会进行 TLS 握手。随后浏览器发送 HTTP 请求,服务端处理并返回响应,浏览器解析 HTML、CSS、JS,构建 DOM/CSSOM,执行布局、绘制和合成,最终展示页面。
5. I/O 多路复用几种选择,区别和优点?#
答:常见有 select、poll、epoll。select 使用固定大小的 fd 集合,每次调用都要拷贝和线性扫描;poll 用数组保存 fd,突破了 select 的 fd_set 大小限制,但仍然需要线性扫描;epoll 通过 epoll_ctl 注册 fd,通过就绪队列返回活跃事件,适合大量连接、少量活跃的高并发网络服务。
6. Go 里 slice 的底层结构是什么?#
答:slice 底层是一个结构体,包含指向数组的指针、长度 len 和容量 cap。len 表示当前可访问元素个数,cap 表示从起始位置到底层数组末尾的容量,append 超过容量时会触发扩容并迁移数据。
7. make([]int, 2) 和 make([]int, 3, 5) 有什么区别?#
答:make([]int, 2) 创建长度为 2、容量为 2 的切片,两个元素都是零值。make([]int, 3, 5) 创建长度为 3、容量为 5 的切片,当前可访问 3 个元素,后续还能在原底层数组上追加 2 个元素。
8. map 并发安全吗?多协程环境下怎么写?#
答:普通 map 在并发读写或并发写时会触发运行时错误。多协程场景常见做法是 map + sync.RWMutex、sync.Map、分片锁 map,或者通过 channel 把 map 访问集中到单个 goroutine。
9. sync.Map 有了解吗?#
答:sync.Map 是 Go 标准库提供的并发安全 map,适合读多写少、key 相对稳定的缓存类场景。底层核心思路是读写分离,读路径优先查只读区 read,写入和读穿透会走加锁路径访问 dirty,miss 次数达到阈值后 dirty 晋升为新的 read。
10. Go 的协程为什么能轻松支持百万高并发?#
答:goroutine 初始栈很小并且可以动态增长,创建和切换成本远低于操作系统线程。Go 运行时通过 GMP 调度模型把大量 G 复用到少量 M 上执行,并结合 work stealing、netpoller、异步抢占和栈管理支撑高并发。
11. 多个子协程怎么实现优雅退出?#
答:常用 context 传递取消信号,用 WaitGroup 等待子协程收尾。子协程在循环里通过 select 监听 ctx.Done(),收到取消信号后释放资源并返回;主协程调用 cancel() 后再 wg.Wait()。
12. 主协程希望子协程提前退出,怎么实现?#
答:用 context.WithCancel 主动取消,或用 context.WithTimeout、context.WithDeadline 设置超时。子协程需要把耗时操作写成可取消形式,在等待 channel、I/O、定时器或任务队列时监听 ctx.Done()。
13. 怎么监控 context 的取消或超时消息?#
答:通过 <-ctx.Done() 监听取消信号,通过 ctx.Err() 判断原因。context.Canceled 表示主动取消,context.DeadlineExceeded 表示超时或截止时间到达,也可以通过 ctx.Deadline() 获取截止时间。
14. 1000 个任务只用 5 个协程执行,怎么实现?#
答:使用 worker pool。创建一个任务 channel,启动 5 个 worker 循环读取任务并执行,主协程把 1000 个任务写入 channel,写完后关闭 channel,最后用 WaitGroup 等待 5 个 worker 退出。
jobs := make(chan int)
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
_ = job // 执行业务逻辑
}
}()
}
for i := 0; i < 1000; i++ {
jobs <- i
}
close(jobs)
wg.Wait()15. Go 里哪些问题可能会导致内存泄露?#
答:常见原因包括 goroutine 阻塞后长期存活、channel 发送或接收无人配对、Ticker 使用后未停止、slice 引用大底层数组、map 或本地缓存持续增长、连接和文件句柄未关闭、context.WithCancel 创建后未调用 cancel。排查时常用 pprof 看 heap、goroutine、allocs 和阻塞栈。
16. Redis 五种数据结构#
答:Redis 五种基础数据结构是 String、Hash、List、Set、ZSet。底层实现分别常见为 SDS、listpack/hashtable、quicklist、intset/hashtable、listpack 或 skiplist + dict。
17. ZSet 底层结构#
答:ZSet 小数据量时使用 listpack,数据量变大后使用 skiplist + dict。dict 负责按 member 快速查 score,skiplist 负责按 score 排序、范围查询和排名相关操作,典型复杂度是 O(logN)。
18. Redis 的 RDB 和 AOF 持久化机制#
答:RDB 是快照持久化,会在某个时间点把内存数据生成紧凑的快照文件,恢复速度快,适合备份和全量恢复。AOF 记录写命令日志,按 always、everysec、no 等策略刷盘,数据完整性更强,文件会通过 rewrite 压缩体积。生产环境常见做法是 RDB + AOF 混合持久化。
19. Redis 数据过期和数据淘汰#
答:过期删除针对设置了 TTL 的 key,Redis 结合惰性删除和定期删除清理过期数据。内存淘汰发生在内存达到 maxmemory 后,按策略释放 key,常见策略有 allkeys-lru、volatile-lru、allkeys-lfu、volatile-lfu、allkeys-random、volatile-random、volatile-ttl 和 noeviction。
20. 10000 个整数找最大的 10 个数#
答:推荐用大小为 10 的小顶堆。先把前 10 个数建堆,之后每来一个数就和堆顶比较,大于堆顶则替换堆顶并调整堆。最终堆中就是最大的 10 个数,时间复杂度 O(NlogK),空间复杂度 O(K),这里 K=10。