Golang错误机制
Go的错误处理哲学
Go的设计者认为:错误是常态,应该显式处理。
// Go的方式:错误是返回值的一部分,你必须处理
result, err := 做某事()
if err != nil {
// 出错了!必须在这里处理
return
}
// 没出错,继续使用result核心原则:不隐藏错误,强制开发者面对问题
在 Go 语言中,错误(Error)就像普通的数据一样被服务员平平静静地“递”回来。Go 鼓励程序员在每一步都主动检查:“刚才的任务成功了吗?”
error接口——错误的本质
在Go中,error就是一个接口:
// error接口的定义(内置在Go中)
type error interface {
Error() string // 只要实现了Error()方法,就是error
}任何类型,只要有一个 Error() string 方法,就可以当作错误使用。
// 定义一个包含状态码的专属错误
type MyVIPError struct {
Code int // 错误代码,比如 404
Message string // 错误信息
}
// 只要它有 "Error()" ,Go 就认它是一个错误
func (e *MyVIPError) Error() string {
// 把代码和信息拼起来
return fmt.Sprintf("错误码: %d, 原因: %s", e.Code, e.Message)
}创建最基本的错误
package main
import (
"errors" // 标准库提供的错误工具包
"fmt"
)
func main() {
// 方式1:使用errors.New创建简单错误
err1 := errors.New(" Something went wrong")
fmt.Println(err1) // 输出: Something went wrong
// 方式2:使用fmt.Errorf创建格式化错误
name := "Alice"
age := -5
err2 := fmt.Errorf("invalid age: %d for user %s", age, name)
fmt.Println(err2) // 输出: invalid age: -5 for user Alice
}errors.New= 写一张简单的便签fmt.Errorf= 写一张可以填入信息的模板便签
函数如何返回错误
多返回值
Go函数可以一次返回多个值,这是错误处理的基础:
package main
import (
"errors"
"fmt"
)
// 函数返回两个值:(结果, 错误)
// 如果出错,结果通常为零值,错误不为nil
// 如果成功,错误为nil
func divide(a, b float64) (float64, error) {
if b == 0 {
// 除数为0,返回错误
return 0, errors.New("cannot divide by zero")
}
// 正常情况,错误返回nil
return a / b, nil
}
func main() {
// 调用函数,接收两个返回值
result, err := divide(10, 2)
if err != nil {
fmt.Println("计算失败:", err)
return
}
fmt.Printf("10 / 2 = %.2f\n", result) // 输出: 10 / 2 = 5.00
// 测试错误情况
result2, err2 := divide(10, 0)
if err2 != nil {
fmt.Println("计算失败:", err2) // 输出: 计算失败: cannot divide by zero
return
}
fmt.Printf("10 / 0 = %.2f\n", result2)
}错误处理的标准模式
// 标准5步曲
result, err := 某个函数()
if err != nil {
// 1. 记录日志
// 2. 清理资源
// 3. 返回错误(或降级处理)
return err
}
// 4. 使用结果
使用(result)
// 5. 继续后续操作关键约定:
- 错误是最后一个返回值
- 错误变量名永远叫
err(约定俗成) - 成功时
err == nil,失败时err != nil
错误的判断与分类
判断错误是否发生
if err != nil {
// 有错误!
}判断具体是什么错误
方式一:错误值比较(Sentinel Errors)
预定义一些"标准错误",像常量一样使用:
package main
import (
"errors"
"fmt"
)
// 在包级别定义标准错误(哨兵错误)
// 习惯用 ErrXXX 命名
var (
ErrNotFound = errors.New("resource not found")
ErrInvalidInput = errors.New("invalid input")
ErrPermissionDenied = errors.New("permission denied")
ErrTimeout = errors.New("operation timeout")
)
// 模拟数据库查询
func findUser(id int) (string, error) {
if id < 1 {
return "", ErrInvalidInput // 返回特定错误
}
if id == 999 {
return "", ErrNotFound // 返回特定错误
}
return "User" + fmt.Sprint(id), nil
}
func main() {
// 测试不同错误
testCases := []int{-5, 999, 42}
for _, id := range testCases {
user, err := findUser(id)
if err != nil {
// 判断具体是哪种错误
if err == ErrInvalidInput {
fmt.Printf("ID=%d: 输入无效,请检查参数\n", id)
} else if err == ErrNotFound {
fmt.Printf("ID=%d: 用户不存在,显示404页面\n", id)
} else {
fmt.Printf("ID=%d: 未知错误: %v\n", id, err)
}
continue
}
fmt.Printf("ID=%d: 找到用户 %s\n", id, user)
}
}输出:
ID=-5: 输入无效,请检查参数
ID=999: 用户不存在,显示404页面
ID=42: 找到用户 User42方式二:errors.Is(Go 1.13+ 推荐)
问题:错误可能被包装,直接比较会失败。
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
func findInDB() error {
return ErrNotFound
}
func findInCache() error {
return fmt.Errorf("cache miss: %w", ErrNotFound) // 包装错误
}
func main() {
err := findInCache()
// 直接比较:失败!因为err是包装后的
fmt.Println("直接比较:", err == ErrNotFound) // false
// 使用errors.Is:成功!会自动"拆包装"
fmt.Println("errors.Is:", errors.Is(err, ErrNotFound)) // true
}errors.Is的作用:像剥洋葱一样,一层层拆开包装,直到找到目标错误。
方式三:errors.As(获取错误的具体类型)
在 Go 1.13 版本之前,如果遇到错误,我们通常只是简单地返回它。但有时候,为了增加更多的上下文信息(比如是在哪个函数、哪个步骤出错的),我们会把原始错误“包装”起来。
以前,直接用类型断言 err.(*PathError) 只能检查最外层。如果错误被套在了里面,断言就会失败。 errors.As 就是为了解决这个问题而诞生的。
errors.As 是一个专门用来“拆套娃并提取内容”的工具。
它的工作原理是:
- 剥开外衣:它会自动一层一层地剥开(Unwrap)错误套娃。
- 寻找目标:在每一层检查,看当前的错误是不是你想要的那个“特定类型”。
- 赋值提取:如果找到了,它就把那个错误赋值给你准备好的变量,并返回
true。如果剥到最里面都没找到,就返回false。
当需要调用错误类型的方法或访问其字段时:
package main
import (
"errors"
"fmt"
"net"
)
func main() {
// 模拟一个网络错误
err := &net.OpError{
Op: "dial",
Net: "tcp",
Err: errors.New("connection refused"),
}
// 我们想判断这是否是网络错误,并获取详细信息
// 方式1:类型断言(旧方式,不推荐)
if netErr, ok := err.(*net.OpError); ok {
fmt.Printf("网络操作: %s, 网络类型: %s\n", netErr.Op, netErr.Net)
}
// 方式2:errors.As(推荐,支持错误链)
var netErr *net.OpError
// 参数1: 被包装的总错误 (套娃)
// 参数2: 目标变量的指针 (必须传指针的地址 &)
if errors.As(err, &netErr) {
fmt.Printf("操作: %s, 类型: %s\n", netErr.Op, netErr.Net)
}
}初学者在使用 errors.As 时,最容易在第二个参数上犯错。请记住这个口诀:“一定要传目标变量的指针(地址)”。
- 为什么? 因为
errors.As是一个函数,如果你只传变量的值进去,它是无法修改你外面的变量的(Go 是值传递)。只有把变量的“内存地址”交出去(用&),errors.As才能顺着地址把找到的错误塞进你的变量里。
错误的创建与包装
创建错误
package main
import (
"errors"
"fmt"
)
func main() {
// 方法1:简单错误
err1 := errors.New("something went wrong")
// 方法2:格式化错误
err2 := fmt.Errorf("user %s not found", "Alice")
// 方法3:包装错误(保留原始错误)
original := errors.New("connection refused")
wrapped := fmt.Errorf("database connection failed: %w", original)
fmt.Println(err1)
fmt.Println(err2)
fmt.Println(wrapped)
// 输出: database connection failed: connection refused
}错误包装的艺术
为什么要包装?
- 添加上下文(在哪里出错的)
- 保留原始错误(用于后续判断)
- 构建错误链
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("record not found")
// 底层函数
func queryDB(id string) error {
return fmt.Errorf("database query failed for id=%s: %w", id, ErrNotFound)
}
// 中层函数
func getUser(id string) error {
if err := queryDB(id); err != nil {
return fmt.Errorf("getUser failed: %w", err)
}
return nil
}
// 顶层函数
func handleRequest(id string) error {
if err := getUser(id); err != nil {
return fmt.Errorf("request processing failed: %w", err)
}
return nil
}
func main() {
err := handleRequest("123")
fmt.Println("=== 完整错误链 ===")
fmt.Println(err)
// 输出: request processing failed: getUser failed: database query failed for id=123: record not found
fmt.Println("\n=== 判断根因 ===")
if errors.Is(err, ErrNotFound) {
fmt.Println("根本原因是:记录不存在")
}
fmt.Println("\n=== 逐层展开 ===")
// 手动展开(实际很少需要)
current := err
for current != nil {
fmt.Printf("→ %v\n", current)
current = errors.Unwrap(current)
}
}输出:
=== 完整错误链 ===
request processing failed: getUser failed: database query failed for id=123: record not found
=== 判断根因 ===
根本原因是:记录不存在
=== 逐层展开 ===
→ request processing failed: getUser failed: database query failed for id=123: record not found
→ getUser failed: database query failed for id=123: record not found
→ database query failed for id=123: record not found
→ record not found关键:使用 %w 动词包装错误,才能用 errors.Is 和 errors.As 追溯。
自定义错误类型
为什么需要自定义错误?
标准错误只是一段文字,但有时我们需要:
- 错误码(给程序判断)
- HTTP状态码(给API返回)
- 详细信息(给日志记录)
- 堆栈信息(给调试)
创建自定义错误类型
package main
import (
"fmt"
"time"
)
// 定义一个结构体作为错误类型
type AppError struct {
Code int // 错误码
Message string // 给用户看的消息
Detail string // 给开发者看的详情
Timestamp time.Time // 发生时间
Path string // 发生位置
}
// 实现error接口(这是关键!)
func (e *AppError) Error() string {
return fmt.Sprintf("[%d] %s: %s (at %s)",
e.Code, e.Message, e.Detail, e.Path)
}
// 构造函数
func NewAppError(code int, message, detail, path string) *AppError {
return &AppError{
Code: code,
Message: message,
Detail: detail,
Timestamp: time.Now(),
Path: path,
}
}
// 使用示例
func doSomething() error {
return NewAppError(
1001, // 错误码
"操作失败", // 用户消息
"数据库连接超时超过30秒", // 详细原因
"/api/v1/users", // 发生位置
)
}
func main() {
err := doSomething()
// 作为普通错误打印
fmt.Println(err)
// 输出: [1001] 操作失败: 数据库连接超时超过30秒 (at /api/v1/users)
// 类型断言获取详细信息
if appErr, ok := err.(*AppError); ok {
fmt.Printf("\n错误码: %d\n", appErr.Code)
fmt.Printf("用户提示: %s\n", appErr.Message)
fmt.Printf("发生时间: %v\n", appErr.Timestamp)
}
}
评论 (0)