Appearance
10|错误处理
运维和后端程序运行过程中,磁盘可能写满、配置文件可能格式错误、远程服务可能拒绝连接、数据库连接可能超时。这些异常不是程序本身的 bug,而是运行环境的不确定性。Go 没有 try-catch 异常机制,普通错误通过返回值传递,程序员的职责是显式检查每个可能出错的操作。这种设计迫使错误处理出现在调用点,不会被深层的异常栈隐藏。
一、error 接口
error 是 Go 标准库里最常用的接口,只有一个方法:
go
type error interface {
Error() string
}任何实现了 Error() string 方法的类型都可以作为错误使用。最简单的错误用 errors.New 创建:
go
package main
import (
"errors"
"fmt"
)
func validatePort(port int) error {
if port < 1 || port > 65535 {
return errors.New("invalid port")
}
return nil
}
func main() {
if err := validatePort(70000); err != nil {
fmt.Println(err)
}
}nil 表示没有错误。Go 里约定俗成:函数返回的最后一个值是 error 类型,为 nil 时调用成功,非 nil 时调用失败。
二、带上下文的错误
错误信息需要包含具体上下文,否则排查时不知道"invalid port"到底是哪个服务的端口。fmt.Errorf 可以格式化错误字符串:
go
func validatePort(port int) error {
if port < 1 || port > 65535 {
return fmt.Errorf("invalid port: %d", port)
}
return nil
}一层层包装错误,形成错误链:
go
data, err := os.ReadFile("/etc/app.conf")
if err != nil {
return fmt.Errorf("read config: %w", err)
}%w 是 Go 1.13 引入的动词,它会保留原始错误,让调用方后续可以用 errors.Is 或 errors.As 判断错误类型。如果用了 %v,原始错误类型会丢失,只剩格式化后的字符串。
三、errors.Is 与 errors.As
errors.Is 判断错误链里是否包含某个特定错误:
go
import "errors"
if errors.Is(err, os.ErrNotExist) {
fmt.Println("file not found")
}即使 err 被包装了多层,errors.Is 也会沿着错误链逐层比对,直到找到匹配或链结束。这比字符串匹配 err.Error() 更可靠——错误信息改了文案,判断逻辑不受影响。
errors.As 把错误链里的某个错误提取到指定类型变量里,用于判断自定义错误类型:
go
type ConfigError struct {
Path string
}
func (e ConfigError) Error() string {
return "config error: " + e.Path
}
func main() {
var cfgErr ConfigError
if errors.As(err, &cfgErr) {
fmt.Println("config path:", cfgErr.Path)
}
}errors.As 的第二个参数必须是指针,因为它需要把提取到的错误写进变量。
四、错误处理模式
提前返回
错误发生后立刻返回,减少嵌套层级:
go
func process(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read: %w", err)
}
result, err := parse(data)
if err != nil {
return fmt.Errorf("parse: %w", err)
}
if err := save(result); err != nil {
return fmt.Errorf("save: %w", err)
}
return nil
}这种写法比大段 if-else 嵌套更易读。每一层的 if err != nil 都在操作之后立即出现,错误处理不远离产生错误的代码。
错误聚合
批量操作时,不想因为单个错误就中止全部,可以把错误收集起来统一返回:
go
type BatchError struct {
Errors []error
}
func (e BatchError) Error() string {
var msgs []string
for _, err := range e.Errors {
msgs = append(msgs, err.Error())
}
return strings.Join(msgs, "; ")
}五、常见错误
用 panic 处理预期错误
go
// 错误:网络超时是可预见的,不应 panic
if err != nil {
panic(err)
}网络波动、服务重启、DNS 延迟都是生产环境的常态。用 panic 处理这些,等于让程序在每次网络抖动时崩溃。
错误信息不带上下文
go
return errors.New("failed") // 太模糊
return fmt.Errorf("check disk %s failed: %w", path, err) // 带上下文生产排查时,日志里"failed"和"check disk /data failed: no such file"的差距,决定了是 1 分钟定位还是 1 小时定位。
忽略 strconv 的错误
go
port, _ := strconv.Atoi(portStr) // 危险:忽略了解析错误输入不是数字时,port 会变成 0,后续逻辑可能把 0 当成正常值处理。
错误链断裂
go
return fmt.Errorf("read failed: %v", err) // %v 丢失了原始错误类型用 %v 包装错误时,调用方无法再用 errors.Is 判断原始错误类型。包装预期错误时始终用 %w。