主题
第08章 面试题精选 —— 错误处理
★ 基础题
Q1:Go 为什么不用异常?
答:Go 设计哲学认为:
- 显式优于隐式:错误处理应该是控制流的一部分,不应被隐藏
- 简单优于灵活:异常 + try-catch 容易写出难以追踪的代码
- 性能可预测:异常的栈展开有运行时开销
Rob Pike:Errors are values. 错误是值,可以被传递、检查、转换。
Q2:error 接口的定义?
go
type error interface {
Error() string
}就这一个方法。 任何实现了 Error() string 的类型都是 error。
Q3:以下三种创建错误的方式有什么区别?
go
errors.New("not found")
fmt.Errorf("not found: %s", id)
fmt.Errorf("not found: %w", err)| 方式 | 用途 |
|---|---|
errors.New | 静态字符串错误 |
fmt.Errorf("...%s/%d...") | 带格式化的错误(不可解包) |
fmt.Errorf("...%w...") | 包装底层错误,形成错误链 |
Q4:以下代码会怎样?
go
data, _ := os.ReadFile("not-exist")
fmt.Println(string(data))答:编译通过,运行时输出空字符串。
问题:错误被丢弃了!如果你需要知道为啥没数据,无法排查。
正确写法:
go
data, err := os.ReadFile("not-exist")
if err != nil {
log.Fatal(err)
}Q5:errors.Is 和 == 比较错误有什么不同?
go
err := fmt.Errorf("wrap: %w", io.EOF)
err == io.EOF // false(包装后不再相等)
errors.Is(err, io.EOF) // true(沿链查找)结论:判断错误链中是否包含某个错误,用 errors.Is,不要用 ==。
★★ 进阶题
Q6:什么是哨兵错误(Sentinel Error)?
go
var ErrNotFound = errors.New("not found")包级别声明的特殊 error 实例,用 errors.Is(err, ErrNotFound) 比较。
例子:io.EOF、sql.ErrNoRows、os.ErrNotExist。
缺点:包之间产生耦合。现代 Go 倾向用 errors.As 配合错误类型。
Q7:写一个错误处理的中间件。
go
func Recovery(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
stack := debug.Stack()
log.Printf("PANIC: %v\n%s", err, stack)
http.Error(w, "Internal Server Error", 500)
}
}()
next.ServeHTTP(w, r)
})
}考点:
- defer + recover 的组合
- 阻止 panic 把整个服务搞挂
- 用
debug.Stack()记录调用栈
Q8:以下代码 panic 吗?
go
func main() {
defer recover() // 注意:是直接调用 recover
panic("oops")
}答:会 panic。
原因:recover 必须在 defer 函数体内调用。这里 recover 是 defer 的对象本身,被推迟调用了,但调用的不是"在 defer 函数里"。
正确写法:
go
defer func() { recover() }()Q9:解释下面的输出。
go
func main() {
defer fmt.Println("1")
defer func() { fmt.Println("2"); panic("x") }()
defer fmt.Println("3")
panic("init")
}输出:
3
2
panic: init
panic: x解析:
- main 触发 panic("init")
- 倒序执行 defer:先打印 3
- 第二个 defer 打印 2,然后 panic("x") —— 这会替换当前的 panic 值
- 第三个 defer 打印 1
- 最终 panic 上抛,Go 显示两个 panic(init 被 x 替换,但前者会作为 cause 一起打出)
Q10:为什么不要用 panic 处理业务错误?
- panic 是异常控制流:调用方不知道你会 panic
- 影响调用方代码结构:必须 defer recover,污染所有上层代码
- 恢复后状态可能不一致:panic 时栈被部分回卷,资源状态难以预知
- 不利于错误编码:API 应该返回明确的错误
业务错误用 error,初始化错误和严重违反契约才用 panic。
★★★ 深度题
Q11:怎么实现一个"批量收集错误"的工具?
Go 1.20+ 有 errors.Join:
go
errs := []error{
err1, err2, err3,
}
combined := errors.Join(errs...)
fmt.Println(combined) // 多行打印每个错误
errors.Is(combined, err2) // true之前的版本可以用 multierror.Append(社区库)或自己实现:
go
type MultiError []error
func (m MultiError) Error() string {
var sb strings.Builder
for _, e := range m {
sb.WriteString(e.Error() + "\n")
}
return sb.String()
}Q12:解释 errors.Is 的实现原理。
简化版:
go
func Is(err, target error) bool {
if target == nil { return err == target }
for {
if err == target { return true }
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// 用 Unwrap 取下一层
unwrapped, ok := err.(interface{ Unwrap() error })
if !ok { return false }
err = unwrapped.Unwrap()
if err == nil { return false }
}
}要点:
- 一层层调 Unwrap
- 遇到自定义
Is(error) bool方法可参与判断 - 直到 unwrap 返回 nil 才停止
Q13:以下场景该用 panic 还是 error?
| 场景 | 选择 |
|---|---|
| 用户输入手机号格式错 | error |
| 数据库连接失败 | error |
| 程序启动时配置文件不存在 | panic(无法继续) |
| map 取值时 key 不存在 | error(业务) |
| slice[100] 越界 | runtime panic(自动) |
| HTTP handler 内部空指针 | runtime panic + recover 中间件兜底 |
| 数学库 sqrt(-1) | NaN 或 error,不应 panic |
Q14:以下两种返回错误的方式有何区别?
go
// A
return nil, fmt.Errorf("xxx: %w", err)
// B
return nil, errors.New(fmt.Sprintf("xxx: %v", err))A 推荐:
- 用
%w包装,保留错误链 - 性能略好(直接保存 error,不调用 Error())
B 不推荐:
- 错误被"扁平化"成字符串,
errors.Is无法识别 - 多一层字符串复制
Q15:Go 1.20 起 errors.Join 怎么用?
go
e1 := errors.New("login failed")
e2 := errors.New("token expired")
e3 := errors.New("user banned")
joined := errors.Join(e1, e2, e3)
fmt.Println(joined)
// login failed
// token expired
// user banned
errors.Is(joined, e2) // true适用于"批量校验"等场景,不再需要第三方库。
📌 速记口诀
"错误是值;用 %w 包;errors.Is/As;panic 谨慎;defer 才能 recover"
🎯 自测练习
- 写一个
Validate(form Form) error,校验失败时收集所有错误(用errors.Join) - 实现一个 retry 函数,遇到
ErrTemporary自动重试 3 次 - 解释为什么
if err == nil在某些"看起来返回 nil"的代码里会 false - 写一个 HTTP 中间件
Recovery,捕获 panic 返回 500
进入第09章 🚀