一、核心概念:Go 没有“异常”,只有“错误”

在其他语言(如 Java、Python)中,我们习惯用 try-catch-finally 来处理异常。但在 Go 语言中,没有这种机制,而是采用显式的错误返回方式。

简单理解:Go 认为错误是正常流程的一部分,而不是“异常情况”。函数执行出错了,就返回一个错误值,由调用方决定如何处理。

二、Go 的错误处理三件套

1️⃣ 错误类型:error 接口

type error interface {
    Error() string  // 返回错误描述信息
}

任何实现了 Error() string 方法的类型都可以作为错误。

2️⃣ 返回错误:函数返回 error

Go 函数通常最后一个返回值是 error 类型,如果没有错误就返回 nil

3️⃣ 处理错误:检查 err != nil

调用函数后,立即判断 err 是否为 nil,不为 nil 就说明有错误。

三、基础示例:从简单到复杂

示例1:最基本的错误处理

package main

import (
    "errors"
    "fmt"
)

// 除法函数:如果除数为0,返回错误
func divide(a, b float64) (float64, error) {
    if b == 0 {
        // 创建一个错误对象
        return 0, errors.New("除数不能为0")
    }
    return a / b, nil  // nil 表示没有错误
}

func main() {
    // 正常情况
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("出错了:", err)
    } else {
        fmt.Println("结果:", result)  // 输出:结果: 5
    }

    // 错误情况
    result, err = divide(10, 0)
    if err != nil {
        fmt.Println("出错了:", err)  // 输出:出错了: 除数不能为0
        // 注意:有错误时,result 是零值(0),不能使用
    }
}

示例2:读取文件(实际场景)

package main

import (
    "fmt"
    "os"
)

func readFile(filename string) {
    // 打开文件
    file, err := os.Open(filename)
    if err != nil {
        // 处理错误:打印并退出
        fmt.Printf("打开文件失败: %v\n", err)
        return
    }
    // 确保函数结束时关闭文件
    defer file.Close()
    
    // 读取文件内容...
    fmt.Println("文件打开成功,可以读取了")
}

func main() {
    readFile("不存在的文件.txt")
    // 输出:打开文件失败: open 不存在的文件.txt: no such file or directory
    
    readFile("存在的文件.txt")  // 假设文件存在
    // 输出:文件打开成功,可以读取了
}

示例3:自定义错误类型(携带更多信息)

package main

import (
    "fmt"
    "time"
)

// 自定义错误结构体,可以包含额外信息
type MyError struct {
    Code    int       // 错误码
    Message string    // 错误信息
    Time    time.Time // 发生时间
}

// 实现 error 接口的 Error 方法
func (e *MyError) Error() string {
    return fmt.Sprintf("[错误码 %d] %s (发生时间: %s)", 
        e.Code, e.Message, e.Time.Format("2006-01-02 15:04:05"))
}

// 业务函数
func process(age int) error {
    if age < 0 {
        return &MyError{
            Code:    1001,
            Message: "年龄不能为负数",
            Time:    time.Now(),
        }
    }
    if age > 150 {
        return &MyError{
            Code:    1002,
            Message: "年龄超出人类范围",
            Time:    time.Now(),
        }
    }
    return nil
}

func main() {
    err := process(-5)
    if err != nil {
        fmt.Println(err)
        // 输出:[错误码 1001] 年龄不能为负数 (发生时间: 2024-01-15 14:30:25)
        
        // 如果需要获取自定义字段,可以使用类型断言
        if myErr, ok := err.(*MyError); ok {
            fmt.Printf("错误码: %d\n", myErr.Code)  // 输出:错误码: 1001
        }
    }
}

四、高级技巧:panic 和 recover(真正的“异常”)

重要提示panic 和 recover 不是常规错误处理方式,只在程序无法继续运行的严重错误时使用(如数组越界、空指针等)。

对比表格

特性 error(常规错误) panic(严重错误)
使用场景 可预见的错误(文件不存在、网络超时) 不可恢复的致命错误(程序bug)
处理方式 调用方主动判断 if err != nil 程序崩溃或被 recover 捕获
是否终止程序 不会 会(除非 recover)
推荐程度 ✅ 经常使用 ⚠️ 极少使用

示例:使用 panic 和 recover

package main

import "fmt"

// 模拟一个可能 panic 的函数
func dangerousFunction(i int) {
    // 创建一个切片,长度只有3
    arr := []int{1, 2, 3}
    
    // 如果索引越界,会 panic
    fmt.Println(arr[i])
}

// 用 recover 捕获 panic
func safeCall(i int) {
    // defer 保证函数退出前执行
    defer func() {
        if r := recover(); r != nil {
            // r 就是 panic 传入的值
            fmt.Printf("捕获到 panic: %v\n", r)
            fmt.Println("程序恢复了,不会崩溃")
        }
    }()
    
    // 调用可能 panic 的函数
    dangerousFunction(i)
}

func main() {
    fmt.Println("程序开始")
    
    safeCall(1)  // 正常:输出 2
    safeCall(5)  // 越界:会触发 recover,不会崩溃
    
    fmt.Println("程序正常结束")  // 这一行依然会执行
}

输出:

程序开始
2
捕获到 panic: runtime error: index out of range [5] with length 3
程序恢复了,不会崩溃
程序正常结束

五、最佳实践总结(小白记住这些)

✅ 应该做的

  1. 函数设计:总是把 error 作为最后一个返回值

  2. 立即检查:调用函数后立即检查 err != nil

  3. 传递错误:自己无法处理的错误,用 return err 向上传递

  4. 提供信息:用 fmt.Errorf() 添加上下文信息

// 好的做法
func readConfig() error {
    data, err := os.ReadFile("config.json")
    if err != nil {
        // 包装错误,添加有用信息
        return fmt.Errorf("读取配置文件失败: %w", err)
    }
    // 处理 data...
    return nil
}

❌ 不应该做的

  1. 不要忽略错误:千万不要用 _ 忽略 error

// 坏做法
result, _ := divide(10, 0)  // 错误被忽略了,result 是零值,可能导致后续bug

// 好做法
result, err := divide(10, 0)
if err != nil {
    // 必须处理错误
}
  1. 不要滥用 panic:99% 的情况应该使用 error

  2. 不要在非异常情况下用 recover:只用在无法提前预防的 panic 中

六、快速记忆口诀

错误处理不用怕,
error 返回带回家,
调用马上 if 检查,
nil 代表没问题啦。
panic 危险不要用,
recover 保命最后挂。

更多推荐