Go学习笔记:错误处理——没有异常,只有 error


📝 本文首发于 栏轩·阁

欢迎访问阅读原文,获取更好的阅读体验。


一、Go 的错误哲学

Go 和其他语言最大的不同:没有异常(Exception) ,没有 try-catch-finally

错误在 Go 里就是一个普通的值 (value),和 intstring 没什么区别:

go 复制代码
// 错误就是一个返回值,和普通值一样处理
result, err := doSomething()
if err != nil {
    // 处理错误
    return err
}

💡 这种设计的核心理念:错误是预期的一部分,不是意外。文件可能不存在、网络可能断开------这些不是"异常",是正常业务流程的一部分。


二、创建错误:errors.New

最基础的创建方式:

go 复制代码
import "errors"

var ErrNotFound = errors.New("资源未找到")

命名规范

导出的错误变量用 Err 开头(其他包能引用):

go 复制代码
// 标准库中的例子
var os.ErrNotExist  = errors.New("file does not exist")
var sql.ErrNoRows   = errors.New("sql: no rows in result set")

// 你自己的包
var ErrNotFound = errors.New("资源未找到")
var ErrAuthFail = errors.New("认证失败")
var ErrInvalid  = errors.New("无效参数")

这些叫哨兵错误(Sentinel Error)------类似常量,用来比较错误类型。


三、判断错误:errors.Is

不能用 == 比较错误(因为包装后会不同),要用 errors.Is

go 复制代码
var ErrNotFound = errors.New("资源未找到")

// 创建错误
err := findUser(999)   // 返回 ErrNotFound

// ❌ 不要这样
if err == ErrNotFound { ... }

// ✅ 正确方式
if errors.Is(err, ErrNotFound) {
    fmt.Println("没找到用户")
}

errors.Is沿着错误链一路找,即使错误被包装过也能识别。


四、自定义错误类型 + errors.As

有时候光一个字符串不够用,需要携带更多信息(比如哪个文件、什么操作):

go 复制代码
type FileError struct {
    Path string   // 哪个文件
    Op   string   // 什么操作
    Err  error    // 原始错误
}

// 实现 error 接口------只要实现这个方法,*FileError 就是 error
func (e *FileError) Error() string {
    return fmt.Sprintf("文件操作失败 [%s] %s: %v", e.Op, e.Path, e.Err)
}

errors.As 提取自定义错误:

go 复制代码
_, err := readFile("secret.txt")

var fileErr *FileError
if errors.As(err, &fileErr) {
    // 现在可以访问 FileError 特有的字段了
    fmt.Printf("文件 %s 在 %s 操作中出错", fileErr.Path, fileErr.Op)
}

errors.Is vs errors.As

函数 用途 类似
errors.Is(err, target) 判断是否是某个特定错误 ==
errors.As(err, &target) 提取某种类型的错误 类型断言(instanceof

五、错误包装:%w

实际项目中,错误极少直接返回,通常需要加上"上下文"信息:

go 复制代码
// 不包装:原始错误直接传上去,调用者不知道在哪出的问题
return err

// ✅ 包装:加上上下文,同时保留原始错误
return fmt.Errorf("读取配置文件失败: %w", err)

%w 的作用:

  • 给错误增加描述信息(上下文)
  • 保留原始错误,errors.Iserrors.As 依然能找到它
go 复制代码
func readConfig() error {
    err := readFile("config.yaml")
    if err != nil {
        return fmt.Errorf("读取配置失败: %w", err)
    }
    return nil
}

err := readConfig()
if errors.Is(err, os.ErrNotExist) {
    // ✅ 仍然能判断,尽管被包装了
    fmt.Println("文件不存在")
}

不加 %w 的情况

如果不想暴露内部错误细节,用 %v 而不是 %w

go 复制代码
// 这样上层只能用 errors.Is 判断 ErrPermission,看不到原始错误
return fmt.Errorf("无权访问: %v", err)

六、错误处理三大原则

原则1:每个错误都必须处理

go 复制代码
// ❌ 绝对不能这样
result, _ := doSomething()

// ✅ 必须检查
result, err := doSomething()
if err != nil {
    // 处理或返回
}

原则2:错误只记录一次

错误不要层层打印,否则日志里一堆重复信息:

go 复制代码
// ❌ 每一层都打印
func a() {
    err := b()
    if err != nil {
        log.Println(err)   // 打印
        return err
    }
}
func b() {
    err := c()
    if err != nil {
        log.Println(err)   // 又打印
        return err
    }
}

// ✅ 只在最终处理的地方打印
func a() {
    err := b()
    if err != nil {
        log.Println(err)   // 只在这里打印
        return err
    }
}
func b() {
    err := c()
    if err != nil {
        return fmt.Errorf("b 执行失败: %w", err)   // 不打印,只包装
    }
}

原则3:出错后程序继续跑

实际项目中,处理一批任务时,某个出错不应该让全部崩溃:

go 复制代码
for _, item := range items {
    err := process(item)
    if err != nil {
        log.Printf("处理 %v 出错: %v", item, err)
        continue    // 跳过这个,处理下一个
    }
}

七、完整示例

go 复制代码
package main

import (
    "errors"
    "fmt"
)

// 哨兵错误
var (
    ErrNotFound = errors.New("资源未找到")
    ErrInvalid  = errors.New("无效参数")
)

// 自定义错误类型
type BizError struct {
    Code    int
    Message string
}

func (e *BizError) Error() string {
    return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}

// 业务函数
func getUser(id int) (string, error) {
    if id <= 0 {
        return "", ErrInvalid
    }
    if id > 100 {
        return "", fmt.Errorf("查询用户失败: %w", ErrNotFound)
    }
    if id == 50 {
        return "", &BizError{Code: 403, Message: "该用户已被禁用"}
    }
    return "pc", nil
}

func main() {
    testCases := []int{1, -1, 999, 50}

    for _, id := range testCases {
        name, err := getUser(id)
        if err == nil {
            fmt.Printf("[%d] ✅ 用户: %s\n", id, name)
            continue
        }

        // 判断具体错误类型
        switch {
        case errors.Is(err, ErrInvalid):
            fmt.Printf("[%d] ⚠️ 无效参数\n", id)
        case errors.Is(err, ErrNotFound):
            fmt.Printf("[%d] ❌ 未找到\n", id)
        default:
            // 自定义错误
            var bizErr *BizError
            if errors.As(err, &bizErr) {
                fmt.Printf("[%d] 🚫 业务错误: %s\n", id, bizErr.Message)
            } else {
                fmt.Printf("[%d] ❓ 未知错误: %v\n", id, err)
            }
        }
    }
}

输出:

复制代码
[1] ✅ 用户: pc
[-1] ⚠️ 无效参数
[999] ❌ 未找到
[50] 🚫 业务错误: 该用户已被禁用

八、小结

知识点 一句话
errors.New("msg") 创建简单错误
fmt.Errorf("...: %w", err) 包装错误,保留链
errors.Is(err, target) 判断是否为目标错误
errors.As(err, &target) 提取特定类型的错误
自定义错误 实现 Error() string 即可成为 error
接口本质 error 是一个接口,任何实现了 Error() string 的类型都是 error
相关推荐
不会代码的小猴2 小时前
7. JSON
开发语言·c++·笔记·qt·算法·json
ttwuai4 小时前
Go 后台接入 SSO 后菜单正常但接口 403,怎么排查权限链路?
开发语言·后端·golang
Canmag 兴隆磁性4 小时前
C 系列充磁机
c语言·开发语言·学习·永磁材料·充磁·退磁
GGMM7894 小时前
富士 EP‑3955D 变频器主板维修调试笔记
笔记·变频器·变频器维修
今儿敲了吗4 小时前
CN——网络层(上)
网络·笔记
PC2005-cloud4 小时前
Go学习笔记:基本概念与项目结构——GOPATH、Go Modules 与常用命令
笔记·学习·golang
ttwuai4 小时前
Go 后台图片上传到对象存储后,预览 403/404 怎么排查?
开发语言·golang
_Narcissus_5 小时前
排序算法总结表格
c语言·数据结构·c++·笔记·算法·排序算法·总结
新手unity自用笔记5 小时前
unity基于Socket的网络学习
网络·网络协议·学习·unity·c#·游戏引擎
zyf1044165 小时前
暑期实践日志 Day32:完成第六章字幕添加,推进工作
学习·计算机网络·剪辑·暑期实践·课题任务