7.4 KiB
id, title, type, domain, stack, tags, level, related, week
| id | title | type | domain | stack | tags | level | related | week | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| backend.go.interview | Go 面试题 | interview | backend |
|
|
intermediate |
|
|
Go 面试题
在本计划里的体现:
todo-api、task-board(第 1~3 周),可选的 Go 网关(第 14 周)。
基础
【基础】切片(slice)和数组有什么区别?append 什么时候扩容?
参考答案:数组长度固定,是值类型,赋值会整体拷贝;切片是对底层数组的引用,包含指针、长度(len)和容量(cap)。append 时如果容量够就直接写入底层数组,不够就分配一个更大的新数组并拷贝(小切片大约翻倍,大切片按更小的比例增长,具体策略随版本调整)。所以 append 的返回值必须接住。
追问:两个切片共享底层数组会有什么问题?→ 修改一个会影响另一个;需要独立副本时用 copy 或 slices.Clone。
【基础】Go 的接口有什么特点?
参考答案:隐式实现——类型只要有接口要求的全部方法,就自动实现了这个接口,不需要声明 implements。好处是解耦:使用方定义小接口,实现方不需要知道这个接口的存在。常见做法是“接受接口,返回具体类型”。
追问:空接口 any 是什么?→ 没有方法的接口,所有类型都实现它;取出具体值要用类型断言 v.(T) 或类型 switch。
【基础】值接收者和指针接收者怎么选?
参考答案:需要修改接收者、或者结构体很大(避免拷贝)时用指针接收者;小的、不可变的值类型可以用值接收者。同一个类型的方法风格保持一致。注意:指针接收者的方法只属于 *T 的方法集,所以只有 *T 实现了相关接口。
追问:同一个类型的方法接收者可以混用吗?→ 语法上可以,但一般建议统一:只要有一个方法需要指针接收者,就全部用指针接收者,避免实现接口时出现困惑。
【基础】Go 为什么没有异常?错误怎么处理?
参考答案:Go 把错误当成普通返回值,强制调用方显式处理,控制流清楚。用 fmt.Errorf("...: %w", err) 包装错误加上下文,用 errors.Is / errors.As 判断。panic 只用于不可恢复的错误,HTTP 服务用中间件 recover 兜底。
追问:errors.Is 和 == 的区别?→ errors.Is 会沿着包装链逐层比较,== 只比较最外层。
【基础】defer 的执行顺序和常见用途?
参考答案:函数返回前执行,多个 defer 后进先出。常用于关闭文件、释放锁、recover、记录耗时。参数在 defer 语句执行时就求值了。
追问:在循环里 defer 关闭文件有什么问题?→ 要等整个函数结束才关闭,循环次数多会耗尽文件句柄;把循环体提取成函数。
进阶
【进阶】goroutine 和线程有什么区别?讲讲 GMP 调度。
参考答案:goroutine 是 Go 运行时管理的轻量级协程,初始栈只有几 KB 且可增长,创建和切换成本远低于系统线程。GMP:G 是 goroutine,M 是系统线程,P 是调度用的处理器(数量默认等于 CPU 核数,GOMAXPROCS)。每个 P 有本地队列,M 绑定 P 执行其中的 G;队列空了会从全局队列或其他 P“偷”任务;G 阻塞在系统调用时,P 会交给其他 M 继续运行。
追问:goroutine 会不会被抢占?→ Go 1.14 起支持基于信号的异步抢占,长时间运行的循环也能被调度出去。
【进阶】channel 的阻塞规则?关闭后会怎样?
参考答案:无缓冲 channel 发送和接收必须同时就绪;有缓冲的满了发送阻塞、空了接收阻塞。关闭后:接收会先读完剩余数据,然后立即返回零值(v, ok := <-ch 中 ok 为 false);向已关闭的 channel 发送会 panic;重复关闭也会 panic;对 nil channel 收发永远阻塞。
追问:谁负责关闭 channel?→ 发送方;多个发送方时用 WaitGroup 等全部结束后由一个协调者关闭。
【进阶】context 有什么用?使用规范是什么?
参考答案:在调用链上传递取消信号、截止时间和请求范围的值。HTTP 请求结束或超时,所有带这个 context 的数据库查询、下游调用都会及时停止,避免资源浪费。规范:作为第一个参数传递、不要存进结构体、不要传 nil(用 context.TODO())、WithValue 只放请求范围的元数据(如请求 ID),不放业务参数。
追问:context 里适合存什么值?→ 只存请求范围的元数据(请求 ID、用户身份、追踪信息),不要用它传递业务参数;key 用自定义的未导出类型,避免冲突。
【进阶】什么是数据竞争?怎么发现和避免?
参考答案:多个 goroutine 同时访问同一变量且至少一个是写,又没有同步。用 go test -race / go run -race 发现(只能发现实际执行到的路径)。避免:用 channel 传递所有权、sync.Mutex / RWMutex 加锁、sync/atomic 原子操作;并发读写 map 会直接 fatal。
追问:-race 能在生产环境开吗?→ 一般不开:会显著增加 CPU 和内存开销。在测试和预发环境开启,CI 中跑 go test -race。
场景题
【场景题】一个接口要并发调用 3 个下游服务,整体 2 秒超时,任一失败就返回错误,怎么写?
参考答案:用 errgroup.WithContext(ctx) 配合 context.WithTimeout(ctx, 2*time.Second):每个下游一个 g.Go(...),任一返回错误时 errgroup 会取消共享的 context,其他调用随之停止;g.Wait() 返回第一个错误。下游客户端必须使用这个 context 才能真正被取消。如果需要“部分失败也返回”,就不用 errgroup 的取消语义,改成各自记录结果(见本技术栈的场景文档)。
追问:下游有一个特别慢但不重要的服务怎么办?→ 给它单独更短的超时,失败时降级(返回默认值)。
【场景题】线上 Go 服务内存持续上涨,怎么排查?
参考答案:先确认是不是泄漏:看监控的内存曲线和 goroutine 数量。用 net/http/pprof 暴露分析接口,抓 heap 看哪些对象占内存、抓 goroutine 看有没有大量阻塞在同一处的 goroutine(最常见的是 channel 没人接收、没设超时的网络请求)。对比两个时间点的 heap 差异定位增长来源。修复后加上超时和 context 取消。
追问:pprof 能直接暴露在公网吗?→ 不能,只在内网或通过鉴权访问。
【场景题】服务发布时如何做到不丢请求?
参考答案:优雅关机:监听 SIGTERM,调用 http.Server.Shutdown(ctx) 停止接收新连接并等待处理中的请求完成(带超时);后台 worker 通过 context 收到取消信号,处理完当前任务再退出;最后关闭数据库连接池。部署侧配合负载均衡的健康检查,先摘流量再停进程。在本计划第 1 周第 7 天实现过。
追问:Kubernetes 环境下还要注意什么?→ 收到 SIGTERM 后先让就绪探针失败,等待负载均衡摘掉流量(或配置 preStop 等待),再关闭服务;terminationGracePeriodSeconds 要大于程序的优雅退出超时。