Skip to content

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
}

实际项目更推荐在启动阶段构造并通过依赖注入传递。

继续阅读