Go 后端面试复盘:语言、并发与服务实践 · 28. 高频面试快问快答至29. 手写题与代码模板
前置:Go 系统学习,并完成一个HTTP 工程练习。本页用于面试前查漏补缺,不按章节替代语言课程。
28. 高频面试快问快答
先遮住右列口述答案,再按链接回看具体规则和代码。表中保留正文之外的补充判断题;已展开的原理直接连接到相应小节。
28.1 基础类
| 自测问题 | 回看或补充答案 |
|---|---|
| Q1:Go 为什么不支持方法重载? | 强调简单、明确和快速编译。不同语义使用不同函数名,或通过接口、泛型和可选配置表达。 |
| Q2:Go 是否支持默认参数? | 不支持。常见替代方案是配置结构体、functional options 或提供多个命名清晰的构造函数。 |
| Q3:Go 是否支持三元运算符? | 不支持,使用 if/else,避免复杂表达式降低可读性。 |
| Q4:可导出标识符规则是什么? | 包级标识符首字母是 Unicode 大写字母时可从其他包访问。 |
Q5:init 能被调用吗? | 不能显式调用,也不能引用。它由运行时在包初始化阶段执行。 初始化规则 |
| Q6:一个包可以有多个 init 吗? | 多个 init 的边界 |
| Q7:main 包必须有什么? | 可执行程序入口需要 package main 和无参数、无返回值的 func main()。 |
Q8:空白标识符 _ 有什么用途? | 忽略值、仅触发包初始化的匿名导入、编译期接口检查等。匿名导入要有明确副作用理由。 |
| Q9:Go 中枚举怎么实现? | 通常使用自定义整数类型加 const/iota,并提供 String、Validate 等方法。对外协议值最好显式固定。 枚举与协议值 |
| Q10:为什么不建议大量使用全局变量? | 显式依赖与构造 |
28.2 slice、map、interface
| 自测问题 | 回看或补充答案 |
|---|---|
| Q11:切片扩容后原切片会变化吗? | append 返回值 |
| Q12:切片作为函数参数能修改元素吗? | 切片传值与共享 |
Q13:copy 是深拷贝吗? | 浅拷贝边界 |
| Q14:map 能边遍历边删除吗? | 语言允许删除尚未遍历到的键,后续是否出现有规定语义;但复杂并发和边遍历边修改容易难以理解。并发修改仍需同步。 |
| Q15:map 预分配容量是精确桶数量吗? | 不是。hint 只是预计元素数量,运行时据此优化初始分配。 容量提示 |
| Q16:接口越大越好吗? | 小接口原则 |
| Q17:接口应该由实现方还是使用方定义? | 使用方定义接口 |
Q18:reflect.DeepEqual 有什么问题? | 语义未必符合业务预期,例如 nil slice 与空 slice 不等。测试优先使用明确字段比较或适合的比较工具和选项。 |
| Q19:为什么 map value 用 struct 指针不一定更快? | 指针减少大结构复制,但增加堆对象、GC 扫描和 cache miss。需要根据大小、修改语义和 benchmark 选择。 |
| Q20:字符串可以包含非法 UTF-8 吗? | 可以。string 是任意字节序列;range 遇到非法序列会产生替换 rune。 |
28.3 并发类
| 自测问题 | 回看或补充答案 |
|---|---|
| Q21:goroutine 是并发还是并行? | goroutine 提供并发结构;当有多个 P、多个 M 和多个 CPU 核时可以并行执行。 |
| Q22:channel 是线程安全的吗? | channel 的发送、接收和关闭由运行时同步,但围绕它的业务协议仍可能有竞态,例如多个发送者争相关闭。 |
| Q23:关闭 channel 后还能读取吗? | 已关闭通道接收 |
| Q24:如何判断 channel 已关闭? | 接收时使用 v, ok := <-ch。没有通用无竞争的“先检查是否关闭再发送”操作。 |
| Q25:buffered channel 满了会怎样? | 缓冲与阻塞 |
| Q26:Mutex 是否公平? | 运行时在正常和饥饿模式间权衡吞吐与公平,不能把它当严格 FIFO 锁。 |
| Q27:Mutex 可以跨 goroutine 解锁吗? | 锁不与特定 goroutine 绑定,技术上可在另一 goroutine 解锁,但这种设计通常难理解,应保持所有权清晰。 |
| Q28:WaitGroup 可以复用吗? | WaitGroup 阶段边界 |
Q29:为什么 Add 一般放在 go 语句前? | Add/Done 时机 |
| Q30:如何限制 goroutine 数量? | 有界任务代码 |
| Q31:context 取消是否同步等待子 goroutine 退出? | 取消信号与实际退出 |
| Q32:select 能保证公平吗? | 多个就绪 case 中做伪随机选择,减少固定偏置,但不提供业务上的严格公平或优先级保证。 |
| Q33:如何实现优先级 channel? | select 本身无严格优先级。可先非阻塞检查高优先级,再进入包含多个 case 的 select,但仍需仔细处理饥饿和竞态;复杂场景用优先队列加协调 goroutine。 |
| Q34:什么是死锁? | 一组执行单元互相等待且没有任何一个能继续。例如锁顺序相反、无接收者发送、等待自己。运行时只能检测部分“所有 goroutine 都睡眠”的情况。 |
| Q35:如何避免锁顺序死锁? | 定义全局锁顺序,所有路径按同一顺序获取;尽量避免嵌套锁;缩小临界区;必要时合并状态所有权。 |
28.4 内存与 GC
| 自测问题 | 回看或补充答案 |
|---|---|
| Q36:局部变量一定在栈上吗? | 逃逸分析 |
| Q37:指针一定导致堆分配吗? | new 与分配位置 |
| Q38:GC 能回收 goroutine 吗? | 不能像普通不可达对象一样“自动结束”正在阻塞的 goroutine。goroutine 必须执行到返回;其持有对象也可能因此长期存活。 goroutine 生命周期 |
| Q39:减少指针为什么可能降低 GC 成本? | GC 需要扫描指针图。更紧凑、少指针的数据结构可减少扫描工作并改善 cache locality。 数据布局与指针 |
Q40:手动调用 runtime.GC() 合适吗? | 普通服务通常不合适,运行时会自动调节。只有非常特殊、阶段明确且经过测量的场景才考虑。 |
| Q41:GC 时业务会完全停止吗? | STW 与并发标记 |
| Q42:内存增长一定是泄漏吗? | RSS 与堆趋势 |
| Q43:怎么区分累计分配和存活内存? | inuse 与 alloc |
28.5 网络与数据库
| 自测问题 | 回看或补充答案 |
|---|---|
| Q44:TCP 为什么可靠? | 可靠字节流 |
| Q45:UDP 一定不可靠吗? | UDP 本身不保证交付、顺序和去重,但应用层可以在其上构建可靠协议,QUIC 就是典型例子。 |
| Q46:Keep-Alive 的作用是什么? | 连接复用 |
| Q47:连接池耗尽时会怎样? | 请求排队等待连接,延迟升高并可能超时。应监控池使用、等待次数/时长,并优化慢查询或调整容量。 连接池与等待 |
| Q48:数据库索引为什么能加速查询? | 通过有序或特定结构减少扫描数据量,但增加存储和写入维护成本。索引是否生效取决于查询条件、选择性和优化器。 |
| Q49:事务越大越好吗? | 事务边界 |
| Q50:先更新 DB 再删缓存为什么仍可能不一致? | 缓存未命中的读请求可能先读到旧数据库值,在写请求更新数据库并删除缓存后才回填;删除失败也会保留旧缓存。需按一致性要求加入版本校验、重试或补偿。见 Cache-Aside 读写顺序。 |
28.6 工程与微服务
| 自测问题 | 回看或补充答案 |
|---|---|
| Q51:为什么要传递 trace ID? | 把一次请求跨服务、队列和数据库调用串联起来,便于定位长尾和错误传播。 跨服务追踪 |
| Q52:日志中能否直接打印请求体? | 不应默认打印。可能包含凭证、隐私和大字段;需要字段白名单、脱敏、采样和大小限制。 日志字段与敏感数据 |
| Q53:服务出现大量超时应该先增加超时吗? | 不一定。先区分正常业务耗时和过载排队。盲目增加超时会让更多请求占用资源,加重雪崩。 |
| Q54:为什么需要背压? | 当消费者跟不上生产者时,让上游减速、拒绝或降级,防止无限积压耗尽资源。 |
| Q55:什么是隔离舱模式? | 资源隔离 |
| Q56:配置热更新如何保证并发安全? | 构建完整不可变配置快照,校验成功后用 atomic.Value 或锁整体替换;读取方只读取,不修改。 不可变快照发布 |
| Q57:为什么接口重试必须考虑幂等? | 重试前提 |
| Q58:什么时候应该拆微服务? | 拆分收益与代价 |
| Q59:什么时候使用消息队列? | 异步解耦、削峰、事件广播、耗时任务、最终一致性。不要仅为“架构高级”引入。 |
| Q60:灰度发布要观察什么? | 错误率、延迟分位数、资源、业务指标、依赖影响、日志异常,并保留快速回滚能力。 |
29. 手写题与代码模板
29.1 并发安全单例
go
type Client struct{}
var (
client *Client
once sync.Once
)
func GetClient() *Client {
once.Do(func() {
client = &Client{}
})
return client
}实际项目更推荐在启动阶段构造并通过依赖注入传递。
继续阅读
- 排错
- 排错
- 排错