01. Go 的错误处理哲学
Go 没有 try-catch 异常机制,错误就是一个普通的值,通过返回值的最后一个参数传递。每个可能出错的函数都返回 (result, error),调用方必须检查 error。这种显式错误处理让代码看起来很啰嗦,但好处是错误处理路径一目了然,不会出现被吞掉的异常。
go
func ReadFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取失败 %s: %w", path, err)
}
return data, nil
}
// 调用方必须检查
content, err := ReadFile("data.txt")
if err != nil {
log.Fatal(err)
}永远不要忽略 error,即使用 _ 丢弃也要想清楚为什么抛弃它。
02. errors 包和 fmt.Errorf
errors.New 创建简单的错误,fmt.Errorf 可以格式化错误信息。Go 1.13 引入了错误包装机制,用 %w 动词可以把底层错误包起来,errors.Is 和 errors.As 用来判断和解包错误链。这样既能添加上下文信息,又不丢失原始错误。
go
// 创建错误
err := errors.New("文件不存在")
// 包装错误
err = fmt.Errorf("打开配置文件失败: %w", err)
// 判断错误类型
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件确实不存在")
}
// 解包获取具体错误
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("路径:", pathErr.Path)
}%w 只能在一层 fmt.Errorf 中用一次,不要包装多个错误。
03. 自定义错误类型
简单的错误用 errors.New 或 fmt.Errorf 就够了。复杂的业务错误需要自己定义错误类型,实现 Error() 方法就可以了。结构体类型的错误可以携带更多上下文信息(比如错误码、请求 ID),方便上层做差异化处理。
go
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("字段 %s 验证失败: %s", e.Field, e.Message)
}
// 使用
func ValidateUser(u User) error {
if u.Name == "" {
return &ValidationError{Field: "Name", Message: "不能为空"}
}
return nil
}
// 类型断言处理
err := ValidateUser(user)
var vErr *ValidationError
if errors.As(err, &vErr) {
fmt.Println("哪个字段:", vErr.Field)
}自定义错误类型建议实现 Is(target error) bool 方法,这样 errors.Is 才能正确匹配。
04. defer、panic 和 recover
defer 延迟执行,在函数返回前按后进先出的顺序执行,常用于关闭资源。panic 是程序崩溃,类似其他语言的 throw,但 Go 不推荐用 panic 处理常规错误。recover 只能在 defer 里用,用来捕获 panic 防止程序崩溃。一般只在包的顶层或框架里用 recover。
go
func ReadConfig() (cfg Config, err error) {
f, err := os.Open("config.json")
if err != nil {
return cfg, err
}
defer f.Close() // 函数返回前自动关闭
// defer + recover 防崩溃
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("解析配置崩溃: %v", r)
}
}()
dec := json.NewDecoder(f)
return cfg, dec.Decode(&cfg)
}panic 不是异常处理机制,只在不可恢复的错误时使用。常规错误请返回 error。
05. 错误处理的最佳实践
错误应该在最早出现的地方包装,添加有意义的上下文,然后一路向上返回。不要在中间层打印日志又返回错误,日志要么在最底层打,要么在最顶层统一处理。用 errors.Is 而不是 == 比较错误,因为可能经过了包装。sentinel error(包级别的错误变量)是常见的模式。
go
// Sentinel error 模式
var ErrNotFound = errors.New("not found")
func FindUser(id int) (*User, error) {
// 没找到
return nil, ErrNotFound
}
// 调用方判断
user, err := FindUser(123)
if errors.Is(err, ErrNotFound) {
// 处理未找到的情况
}
// 不要这样 - 中间层既打日志又返回错误
if err != nil {
log.Println(err) // 坏习惯
return err
}
// 应该让调用方决定怎么处理错误错误信息用英文小写开头,不要标点结尾,保持简洁。如 fmt.Errorf("open file: %w", err)。
知识测验
第 1/4 题正确 0
Go 中错误处理的基本模式?
下一节
下一节 Web 开发