> 错误处理不是try-catch那么简单的事儿
写在前面
前两天线上服务又双叒叕挂了,排查了半天才发现——某个 goroutine 里的错误被"优雅地"忽略了。是的,你没看错,错误被静默吞掉了,连个日志都没有。
这让我想起来一个经典段子:
「我的程序从来没有错误,只是有些我不知道的行为。」
今天咱们来聊聊 Go 里的错误处理,那些面试官不会问、但线上会教的坑。
Go 错误处理的"佛系"哲学
Go 的错误处理可能是最"反直觉"的设计之一了。没有 try-catch,没有异常机制,错误就是一个普普通通的 error 接口。
type error interface {
Error() string
}
就这?是的,就这。
很多人吐槽 Go 错误处理太繁琐,满屏的 if err != nil 看的人头皮发麻。但我要说,这恰恰是 Go 的设计哲学——错误是值,不是异常。
什么意思?异常意味着"例外",意味着"我不该处理"。但 Go 告诉你,错误就是一种普通的返回值,和 int、string 没区别。你得重视它,处理它。
那些年我们踩过的错误处理坑
坑一:错误被静默吞掉
// ❌ 典型反面教材
func process(data []byte) {
result, err := parse(data)
// 错误?不存在的好吧
fmt.Println(result)
}
这种代码在生产环境就是定时炸弹。你永远不知道什么时候会出错,出了什么错。
// ✅ 最低限度:至少打个日志
func process(data []byte) {
result, err := parse(data)
if err != nil {
log.Printf("parse failed: %v", err)
return
}
fmt.Println(result)
}
坑二:wrapping 过度,丢失上下文
// ❌ 错误信息成了俄罗斯套娃
func outer() error {
if err := inner(); err != nil {
return fmt.Errorf("inner failed: %v", err)
}
return nil
}
// 打印出来是这样的:
// inner failed: database failed: connection refused
// ...请问到底是哪一层挂了?
Go 1.13 引入了 %w 包装错误,解决了这个问题:
// ✅ 正确姿势
func outer() error {
if err := inner(); err != nil {
return fmt.Errorf("outer 调用 inner 失败: %w", err)
}
return nil
}
用 %w 包装的错误可以用 errors.Is() 和 errors.As() 来检查,再也不用字符串匹配了。
坑三:Goroutine 里的错误是无底洞
// ❌ 经典错误:goroutine 的错误石沉大海
func fetchAll(urls []string) []Result {
results := make([]Result, 0)
for _, url := range urls {
go func(u string) {
r, err := http.Get(u)
if err != nil {
// 错误去哪了?
// 答:去了虚空
}
results = append(results, parseResponse(r))
}(url)
}
return results // 恭喜你,拿到了一个竞态条件的 results
}
goroutine 里的错误处理是个大难题。推荐几种方案:
方案一:使用 channel 收集错误
func fetchAll(urls []string) ([]Result, error) {
results := make([]Result, 0, len(urls))
errCh := make(chan error, len(urls))
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go func(u string) {
defer wg.Done()
r, err := http.Get(u)
if err != nil {
errCh <- fmt.Errorf("fetch %s failed: %w", u, err)
return
}
results = append(results, parseResponse(r))
}(url)
}
wg.Wait()
close(errCh)
// 检查有没有错误
for err := range errCh {
log.Println(err)
}
return results, nil
}
方案二:使用 errgroup
func fetchAll(urls []string) ([]Result, error) {
ctx := context.Background()
g, ctx := errgroup.WithContext(ctx)
results := make([]Result, len(urls))
for i, url := range urls {
i, url := i, url // 避免闭包问题
g.Go(func() error {
r, err := http.Get(url)
if err != nil {
return fmt.Errorf("fetch %s failed: %w", url, err)
}
results[i] = parseResponse(r)
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err // 自动取消其他 goroutine
}
return results, nil
}
errgroup 是 golang.org/x/sync/errgroup,自动处理了 wait、错误传播和 context 取消,简直是并发错误处理的救星。
错误处理的最佳实践
1. Sentinel Error 是你的好朋友
// 定义预定义的错误
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
ErrTimeout = errors.New("operation timeout")
)
// 使用 errors.Is 检查
if errors.Is(err, ErrNotFound) {
// 处理资源不存在的情况
}
预定义错误让错误处理有据可依,代码可读性 up up。
2. 自定义错误类型
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
// 使用 errors.As 检查
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("验证失败字段: %s\n", valErr.Field)
}
自定义错误类型可以携带更多上下文信息,调用方可以根据错误类型做不同的处理。
3. 错误也要有层级
// 底层库返回底层错误
func lowLevel() error {
return fmt.Errorf("connection refused: %w", net.ErrConnectionRefused)
}
// 中间层包装业务错误
func middleLevel() error {
if err := lowLevel(); err != nil {
return fmt.Errorf("调用用户服务失败: %w", err)
}
return nil
}
// 顶层处理具体的业务逻辑
func topLevel() error {
if err := middleLevel(); err != nil {
if errors.Is(err, net.ErrConnectionRefused) {
return fmt.Errorf("用户服务不可用,请稍后重试: %w", err)
}
return err
}
return nil
}
每层只添加自己关心的信息,保留底层错误信息,用 errors.Is() 做精确判断。
4. 日志还是返回?成年人不做选择
func processWithLogging(data []byte) error {
result, err := parse(data)
if err != nil {
// 记录日志 + 返回错误
log.Printf("parse failed, data length: %d, error: %v", len(data), err)
return fmt.Errorf("parse data failed: %w", err)
}
return nil
}
原则是:记录日志是为了调试,返回错误是为了让调用方知道出错了。两者不冲突。
写在最后
Go 的错误处理确实没有 Java 的 try-catch 那么"优雅",满屏的 if err != nil 确实看着烦。但正是这种"繁琐",强迫你直面每一个可能的错误,而不是假装它不存在。
记住这句话:任何一个被忽略的错误,都可能在某个夜深人静的时候,给你发来一条报警短信。
好好处理错误,程序才能稳定运行。毕竟——
没有错误的人生是假的,没有错误处理程序也是。