错误处理
Go没有异常!用返回值处理错误。刚开始会觉得啰嗦,但习惯后你会爱上它的明确性。
Go的错误哲学
Go相信显式优于隐式。错误是程序的一部分,应该明确处理而不是抛来抛去。
// Go的方式
result, err := doSomething()
if err != nil {
// 处理错误
return err
}
// 使用result
Java/JavaScript程序员注意
// Java - try-catch
try {
result = doSomething();
} catch (Exception e) {
// 处理异常
}
// JavaScript - try-catch 或 Promise
try {
const result = await doSomething();
} catch (error) {
// 处理错误
}
Go没有try-catch!错误作为返回值返回,强制你处理。
error 接口
Go的错误就是一个接口:
type error interface {
Error() string
}
任何实现了 Error() 方法的类型都是error。
创建错误
使用 errors.New
import "errors"
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("除数不能为0")
}
return a / b, nil
}
使用 fmt.Errorf
import "fmt"
func getUser(id int) (*User, error) {
if id <= 0 {
return nil, fmt.Errorf("无效的用户ID: %d", id)
}
// ...
}
// 包装错误(Go 1.13+)
func processUser(id int) error {
user, err := getUser(id)
if err != nil {
return fmt.Errorf("处理用户失败: %w", err) // %w 包装错误
}
// ...
}
错误处理模式
基本模式
result, err := someFunction()
if err != nil {
return err // 向上传递
}
// 使用result
立即返回
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return err
}
err = processData(data)
if err != nil {
return err
}
return nil
}
添加上下文
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("打开文件 %s 失败: %w", filename, err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("读取文件内容失败: %w", err)
}
return nil
}
自定义错误类型
简单自定义错误
type ValidationError struct {
Field string
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("验证失败 [%s]: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return ValidationError{Field: "age", Message: "年龄不能为负数"}
}
if age > 150 {
return ValidationError{Field: "age", Message: "年龄不能超过150"}
}
return nil
}
带错误码的错误
type AppError struct {
Code int
Message string
Err error // 原始错误
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// 实现Unwrap以支持errors.Is和errors.As
func (e *AppError) Unwrap() error {
return e.Err
}
// 预定义错误
var (
ErrNotFound = &AppError{Code: 404, Message: "资源不存在"}
ErrUnauthorized = &AppError{Code: 401, Message: "未授权"}
ErrInternal = &AppError{Code: 500, Message: "内部错误"}
)
func getResource(id string) error {
// ...
if notFound {
return &AppError{
Code: 404,
Message: "资源不存在",
Err: fmt.Errorf("ID: %s", id),
}
}
return nil
}
错误判断
errors.Is - 判断错误类型
import "errors"
var ErrNotFound = errors.New("not found")
func findUser(id int) error {
return ErrNotFound
}
func main() {
err := findUser(1)
// 判断是否是特定错误
if errors.Is(err, ErrNotFound) {
fmt.Println("用户不存在")
}
}
errors.As - 提取错误类型
type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation error: %s", e.Field)
}
func validate() error {
return &ValidationError{Field: "email"}
}
func main() {
err := validate()
// 提取特定类型的错误
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("字段 %s 验证失败\n", valErr.Field)
}
}
哨兵错误(Sentinel Errors)
预定义的错误值,用于比较:
package mypackage
import "errors"
// 哨兵错误(首字母大写,可导出)
var (
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
ErrInvalidInput = errors.New("invalid input")
)
func GetItem(id string) (*Item, error) {
item := findById(id)
if item == nil {
return nil, ErrNotFound
}
return item, nil
}
// 使用
item, err := GetItem("123")
if err != nil {
if errors.Is(err, ErrNotFound) {
// 处理不存在的情况
}
return err
}
panic 和 recover
panic - 程序崩溃
panic用于不可恢复的错误:
func mustOpen(filename string) *os.File {
f, err := os.Open(filename)
if err != nil {
panic(err) // 程序崩溃
}
return f
}
何时使用panic
- 程序初始化时的致命错误
- 不可能发生的情况(表示程序bug)
- 快速失败比继续运行更好
一般业务代码不要使用panic,用error返回!
recover - 恢复崩溃
func safeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
fn()
return nil
}
func main() {
err := safeCall(func() {
panic("something went wrong")
})
if err != nil {
fmt.Println("捕获到panic:", err)
}
fmt.Println("程序继续运行")
}
实战案例:用户注册
package main
import (
"errors"
"fmt"
"regexp"
"strings"
)
// 验证错误
type ValidationErrors struct {
Errors map[string]string
}
func (e *ValidationErrors) Add(field, message string) {
if e.Errors == nil {
e.Errors = make(map[string]string)
}
e.Errors[field] = message
}
func (e *ValidationErrors) HasErrors() bool {
return len(e.Errors) > 0
}
func (e *ValidationErrors) Error() string {
var sb strings.Builder
sb.WriteString("验证失败:\n")
for field, msg := range e.Errors {
sb.WriteString(fmt.Sprintf(" - %s: %s\n", field, msg))
}
return sb.String()
}
// 业务错误
var (
ErrUserExists = errors.New("用户已存在")
ErrEmailExists = errors.New("邮箱已被注册")
ErrWeakPassword = errors.New("密码强度不够")
)
// 用户
type User struct {
Username string
Email string
Password string
}
// 模拟数据库
var users = make(map[string]*User)
var emails = make(map[string]bool)
// 验证用户输入
func validateUser(user *User) error {
errs := &ValidationErrors{}
// 验证用户名
if len(user.Username) < 3 {
errs.Add("username", "用户名至少3个字符")
} else if len(user.Username) > 20 {
errs.Add("username", "用户名最多20个字符")
}
// 验证邮箱
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(user.Email) {
errs.Add("email", "邮箱格式不正确")
}
// 验证密码
if len(user.Password) < 6 {
errs.Add("password", "密码至少6个字符")
}
if errs.HasErrors() {
return errs
}
return nil
}
// 检查密码强度
func checkPasswordStrength(password string) error {
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
hasDigit := regexp.MustCompile(`[0-9]`).MatchString(password)
if !hasUpper || !hasLower || !hasDigit {
return ErrWeakPassword
}
return nil
}
// 注册用户
func register(user *User) error {
// 1. 验证输入
if err := validateUser(user); err != nil {
return fmt.Errorf("输入验证失败: %w", err)
}
// 2. 检查密码强度
if err := checkPasswordStrength(user.Password); err != nil {
return fmt.Errorf("密码检查失败: %w", err)
}
// 3. 检查用户名是否存在
if _, exists := users[user.Username]; exists {
return ErrUserExists
}
// 4. 检查邮箱是否存在
if emails[user.Email] {
return ErrEmailExists
}
// 5. 保存用户
users[user.Username] = user
emails[user.Email] = true
return nil
}
func main() {
testCases := []User{
{Username: "ab", Email: "invalid", Password: "123"}, // 验证失败
{Username: "zhangsan", Email: "test@example.com", Password: "123456"}, // 密码弱
{Username: "zhangsan", Email: "test@example.com", Password: "Test123"}, // 成功
{Username: "zhangsan", Email: "test2@example.com", Password: "Test456"}, // 用户已存在
{Username: "lisi", Email: "test@example.com", Password: "Test456"}, // 邮箱已存在
}
for i, user := range testCases {
fmt.Printf("\n===== 测试 %d =====\n", i+1)
fmt.Printf("用户: %s, 邮箱: %s\n", user.Username, user.Email)
err := register(&user)
if err != nil {
// 根据错误类型处理
var valErrs *ValidationErrors
if errors.As(err, &valErrs) {
fmt.Println("❌ 验证错误:")
for field, msg := range valErrs.Errors {
fmt.Printf(" - %s: %s\n", field, msg)
}
} else if errors.Is(err, ErrWeakPassword) {
fmt.Println("❌ 密码太弱,需要包含大小写字母和数字")
} else if errors.Is(err, ErrUserExists) {
fmt.Println("❌ 用户名已被占用")
} else if errors.Is(err, ErrEmailExists) {
fmt.Println("❌ 邮箱已被注册")
} else {
fmt.Printf("❌ 未知错误: %v\n", err)
}
} else {
fmt.Println("✅ 注册成功!")
}
}
}
实战案例:HTTP客户端重试
package main
import (
"errors"
"fmt"
"math/rand"
"time"
)
// 自定义错误
type RetryableError struct {
Err error
Attempt int
}
func (e *RetryableError) Error() string {
return fmt.Sprintf("attempt %d failed: %v", e.Attempt, e.Err)
}
func (e *RetryableError) Unwrap() error {
return e.Err
}
// 模拟HTTP请求(随机失败)
func httpGet(url string) (string, error) {
// 模拟50%失败率
if rand.Float32() < 0.5 {
return "", errors.New("connection timeout")
}
return fmt.Sprintf("Response from %s", url), nil
}
// 带重试的请求
func httpGetWithRetry(url string, maxRetries int) (string, error) {
var lastErr error
for i := 1; i <= maxRetries; i++ {
fmt.Printf(" 尝试 %d/%d...\n", i, maxRetries)
result, err := httpGet(url)
if err == nil {
return result, nil
}
lastErr = &RetryableError{Err: err, Attempt: i}
// 指数退避
if i < maxRetries {
backoff := time.Duration(i*100) * time.Millisecond
fmt.Printf(" 失败,%v后重试\n", backoff)
time.Sleep(backoff)
}
}
return "", fmt.Errorf("重试%d次后失败: %w", maxRetries, lastErr)
}
// 优雅的错误处理
func fetchData(url string) error {
fmt.Printf("请求: %s\n", url)
result, err := httpGetWithRetry(url, 3)
if err != nil {
// 检查是否是重试错误
var retryErr *RetryableError
if errors.As(err, &retryErr) {
fmt.Printf("最后一次尝试 (attempt %d) 失败\n", retryErr.Attempt)
}
return err
}
fmt.Printf("成功: %s\n", result)
return nil
}
func main() {
rand.Seed(time.Now().UnixNano())
urls := []string{
"https://api.example.com/users",
"https://api.example.com/orders",
}
for _, url := range urls {
fmt.Println("\n" + strings.Repeat("=", 40))
if err := fetchData(url); err != nil {
fmt.Printf("❌ 请求失败: %v\n", err)
} else {
fmt.Println("✅ 请求成功")
}
}
}
错误处理最佳实践
1. 错误要处理或传播
// ❌ 忽略错误
result, _ := riskyOperation()
// ✅ 处理或返回
result, err := riskyOperation()
if err != nil {
return err
}
2. 添加上下文
// ❌ 直接返回
return err
// ✅ 添加上下文
return fmt.Errorf("处理用户 %d 失败: %w", userID, err)
3. 只处理一次
// ❌ 重复处理
if err != nil {
log.Println(err) // 日志
return err // 又返回
}
// ✅ 要么处理,要么返回
if err != nil {
return fmt.Errorf("operation failed: %w", err)
}
4. 使用哨兵错误进行比较
// ❌ 字符串比较
if err.Error() == "not found" {
// ...
}
// ✅ 使用errors.Is
if errors.Is(err, ErrNotFound) {
// ...
}
练习
- 实现一个
divide函数,处理除零错误 - 创建一个自定义错误类型
HTTPError,包含状态码和消息 - 实现一个文件读取函数,处理各种可能的错误
参考答案
package main
import (
"errors"
"fmt"
"os"
)
// 1. 除法函数
var ErrDivideByZero = errors.New("除数不能为零")
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, ErrDivideByZero
}
return a / b, nil
}
// 2. HTTPError
type HTTPError struct {
StatusCode int
Message string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d: %s", e.StatusCode, e.Message)
}
func NewHTTPError(code int, message string) *HTTPError {
return &HTTPError{StatusCode: code, Message: message}
}
// 3. 文件读取
func readFile(filename string) ([]byte, error) {
// 检查文件是否存在
info, err := os.Stat(filename)
if err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("文件不存在: %s", filename)
}
return nil, fmt.Errorf("获取文件信息失败: %w", err)
}
// 检查是否是目录
if info.IsDir() {
return nil, fmt.Errorf("%s 是目录,不是文件", filename)
}
// 读取文件
data, err := os.ReadFile(filename)
if err != nil {
if os.IsPermission(err) {
return nil, fmt.Errorf("没有读取权限: %s", filename)
}
return nil, fmt.Errorf("读取文件失败: %w", err)
}
return data, nil
}
func main() {
// 1. 测试除法
result, err := divide(10, 0)
if errors.Is(err, ErrDivideByZero) {
fmt.Println("捕获到除零错误")
}
result, err = divide(10, 3)
if err == nil {
fmt.Printf("10 / 3 = %.2f\n", result)
}
// 2. 测试HTTPError
err = NewHTTPError(404, "页面未找到")
fmt.Println(err)
// 3. 测试文件读取
_, err = readFile("不存在的文件.txt")
fmt.Println(err)
# 错误处理
Go没有异常!用返回值处理错误。刚开始会觉得啧喘,但习惯后你会爱上它的明确性。我从 Java 转过来的时候,最不适应的就是没有 try-catch。但后来发现,明确地处理每一个错误,虽然代码多了点,但逻辑更清晰,也更不容易忙记错误处理。
## Go的错误哲学
错误处理虽然繁琐,但让代码更可靠!下一节学习[Goroutine](./04-goroutines.md)——Go的并发魔法!
