Go 没有 try-catch,错误处理全靠返回值。刚从 Java 转过来时觉得啰嗦,用久了反而觉得这种显式处理挺清晰的。记录一下 Go 错误处理的核心模式。
error 接口
Go 的 error 就是一个接口:
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都是 error。最简单的创建方式:
import "errors"
err := errors.New("something went wrong")
或者用 fmt.Errorf 带格式化:
err := fmt.Errorf("failed to open file %s: permission denied", filename)
错误包装(Error Wrapping)
Go 1.13 引入了 %w 动词,可以把底层错误包装起来,保留错误链:
file, err := os.Open(path)
if err != nil {
return fmt.Errorf("loadConfig: %w", err)
}
这样上层代码既能看到 loadConfig: open /etc/app.conf: no such file or directory 这样的完整上下文,又能通过 errors.Is / errors.As 检查底层错误。
errors.Is 和 errors.As
// errors.Is 检查错误链中是否包含特定错误值
if errors.Is(err, os.ErrNotExist) {
log.Println("file does not exist, using defaults")
return defaultConfig, nil
}
// errors.As 检查错误链中是否包含特定类型
var pathErr *os.PathError
if errors.As(err, &pathErr) {
log.Printf("path error on %s: %v", pathErr.Path, pathErr.Err)
}
不要用 == 比较错误,因为包装后的错误不再和原始值相等。始终用 errors.Is。
自定义错误类型
当需要携带结构化信息时,定义自己的错误类型:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Message: fmt.Sprintf("invalid value: %d", age),
}
}
return nil
}
调用方可以用 errors.As 提取:
err := validateAge(-1)
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("field %s is invalid: %s\n", ve.Field, ve.Message)
}
Sentinel Error
预定义的错误值叫 sentinel error,标准库里有很多:
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInternal = errors.New("internal error")
)
func GetUser(id int) (*User, error) {
user, ok := db[id]
if !ok {
return nil, fmt.Errorf("GetUser(%d): %w", id, ErrNotFound)
}
return user, nil
}
sentinel error 应该以 Err 开头,导出给调用方使用。
实践建议
- 不要忽略错误——
_ = doSomething()是代码审查红旗 - 添加上下文再返回——用
fmt.Errorf("xxx: %w", err)而不是直接return err - 只在顶层记日志——中间层只负责包装和传递,避免同一个错误被 log 多次
- errors.New 用于定义包级别的 sentinel error,
fmt.Errorf用于运行时动态错误 - 不要过度包装——如果上下文已经足够清晰,不需要每一层都加 wrap
Go 的错误处理确实比 try-catch 多写几行代码,但换来的是每个错误路径都被显式考虑,不会出现"忘了 catch 某个异常"的情况。