Skip to content

第08章 面试题精选 —— 错误处理


★ 基础题

Q1:Go 为什么不用异常?

:Go 设计哲学认为:

  1. 显式优于隐式:错误处理应该是控制流的一部分,不应被隐藏
  2. 简单优于灵活:异常 + try-catch 容易写出难以追踪的代码
  3. 性能可预测:异常的栈展开有运行时开销

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.EOFsql.ErrNoRowsos.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

解析

  1. main 触发 panic("init")
  2. 倒序执行 defer:先打印 3
  3. 第二个 defer 打印 2,然后 panic("x") —— 这会替换当前的 panic 值
  4. 第三个 defer 打印 1
  5. 最终 panic 上抛,Go 显示两个 panic(init 被 x 替换,但前者会作为 cause 一起打出)

Q10:为什么不要用 panic 处理业务错误?

  1. panic 是异常控制流:调用方不知道你会 panic
  2. 影响调用方代码结构:必须 defer recover,污染所有上层代码
  3. 恢复后状态可能不一致:panic 时栈被部分回卷,资源状态难以预知
  4. 不利于错误编码: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 }
    }
}

要点:

  1. 一层层调 Unwrap
  2. 遇到自定义 Is(error) bool 方法可参与判断
  3. 直到 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"


🎯 自测练习

  1. 写一个 Validate(form Form) error,校验失败时收集所有错误(用 errors.Join
  2. 实现一个 retry 函数,遇到 ErrTemporary 自动重试 3 次
  3. 解释为什么 if err == nil 在某些"看起来返回 nil"的代码里会 false
  4. 写一个 HTTP 中间件 Recovery,捕获 panic 返回 500

进入第09章 🚀