Files
zhuangang/topics/backend/go/interview.md
T

86 lines
6.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: backend.go.interview
title: Go 面试题
type: interview
domain: backend
stack: [go]
tags: [Go, 面试]
level: intermediate
related: [backend.go.basics-types, backend.go.errors-concurrency, backend.go.testing-engineering]
week: [1, 3, 16]
---
# 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),不放业务参数。
### 【进阶】什么是数据竞争?怎么发现和避免?
**参考答案**:多个 goroutine 同时访问同一变量且至少一个是写,又没有同步。用 `go test -race` / `go run -race` 发现(只能发现实际执行到的路径)。避免:用 channel 传递所有权、`sync.Mutex` / `RWMutex` 加锁、`sync/atomic` 原子操作;并发读写 map 会直接 fatal。
## 场景题
### 【场景题】一个接口要并发调用 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 天实现过。