Go语言快速入门Go语言快速入门
首页
基础篇
进阶篇
高阶篇
实战篇
Go官方网站
编程指南
首页
基础篇
进阶篇
高阶篇
实战篇
Go官方网站
编程指南
  • 进阶篇

    • 🚀 进阶篇
    • 方法
    • 接口
    • 错误处理
    • Goroutine
    • Channel
    • 包管理
    • 单元测试

错误处理

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) {
    // ...
}

练习

  1. 实现一个 divide 函数,处理除零错误
  2. 创建一个自定义错误类型 HTTPError,包含状态码和消息
  3. 实现一个文件读取函数,处理各种可能的错误
参考答案
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的并发魔法!


最近更新: 2025/12/27 13:26
Contributors: 王长安
Prev
接口
Next
Goroutine