

💡 核心概念: Go 语言中,错误(Error)是一个内置的接口类型。通常函数会返回最后一个值作为 error。我们通过显式检查 if err != nil 来处理错误。Go 1.13 引入了强大的错误包装和检查机制(Is/As)。
Go 的函数通常会返回两个值:一个是结果,一个是错误。如果一切正常,错误就是 nil(无)。
package main
import (
"errors"
"fmt"
)
// 试图打开药柜
// 返回值:(结果 string, 错误 error)
func OpenCabinet(key string) (string, error) {
if key != "9527" {
// 创建一个新错误
// 就像医生写诊断书一样
return "", errors.New("钥匙不对,打不开药柜!")
}
return "药柜开了,取出解药原料", nil
}
func main() {
result, err := OpenCabinet("1234")
// 必须检查错误!这是 Go 程序员的基本素养
if err != nil {
fmt.Println("出事了:", err) // 处理错误
return
}
fmt.Println(result) // 只有没错误时才使用结果
}

err。记住,Never ignore errors! 除非你确信那个错误无关紧要(比如关闭文件时的错误有时候可以忽略,但最好还是记录一下)。
有时候,我们收到错误后,想加点料再往上报。比如“配药失败”,是因为“柜子打不开”,而“柜子打不开”是因为“钥匙断了”。我们需要保留原始错误的信息。
Go 1.13 引入了 %w 动词来包装错误,就像俄罗斯套娃。
func MakeAntidote() error {
_, err := OpenCabinet("1234")
if err != nil {
// %w = Wrap (包装)
// 就像把原始错误包在一个新盒子里,并贴上新的标签
return fmt.Errorf("配药失败: %w", err)
}
return nil
}
func main() {
err := MakeAntidote()
if err != nil {
fmt.Println(err)
// 输出:配药失败: 钥匙不对,打不开药柜!
// 🕵️♂️ 透视眼 (errors.Unwrap)
// 看看里面到底包着什么原始错误
originErr := errors.Unwrap(err)
fmt.Println("根源是:", originErr)
}
}
当错误被层层包装后,直接用 == 比较可能就不行了。这时候我们需要用 errors.Is 和 errors.As。
==,但能穿透包装)。// 定义一个“哨兵错误” (Sentinel Error)
var ErrKeyBroken = errors.New("钥匙断了")
func OpenDoor() error {
// 模拟发生错误,并包装它
return fmt.Errorf("门打不开: %w", ErrKeyBroken)
}
func main() {
err := OpenDoor()
// ❌ 错误做法:直接比较
// if err == ErrKeyBroken { ... } // 这会返回 false,因为 err 被包装过了
// ✅ 正确做法:使用 Is
if errors.Is(err, ErrKeyBroken) {
fmt.Println("确实是钥匙断了!快找锁匠!")
}
}
普通的 error 只是个字符串。如果我们想携带更多信息(比如毒性等级、错误代码),可以自定义结构体来实现 error 接口。
// 定义一种特殊的毒药错误
type PoisonError struct {
Name string // 毒药名字
Level int // 毒性等级
}
// 实现 error 接口的 Error() 方法
func (e *PoisonError) Error() string {
return fmt.Sprintf("中毒了!毒药:%s (等级 %d)", e.Name, e.Level)
}
func CheckPulse() error {
return &PoisonError{Name: "含笑半步癫", Level: 99}
}
func main() {
err := CheckPulse()
// 🕵️♂️ 验尸 (errors.As)
// 尝试把 err 转成 PoisonError 类型,如果成功,poison 变量会被赋值
var poison *PoisonError
if errors.As(err, &poison) {
fmt.Printf("居然是 %s!等级高达 %d!快找解药!\n", poison.Name, poison.Level)
}
}

errors.As 就像是“照妖镜”,不管错误伪装成什么样,只要它本质是那个类型,就能把它现出原形。
有些错误是致命的,比如数组越界、空指针引用,或者你自己手动调用 panic。一旦 Panic 发生,程序就像疯了一样,停止执行当前函数,并逐层向上崩溃,直到整个程序退出。
func main() {
fmt.Println("开始练功...")
// 手动触发崩溃
// 注意:除非无法挽回,否则不要轻易使用 panic
panic("不好!走火入魔了!")
fmt.Println("这就永远不会执行了...")
}
怎么在程序崩溃前把它救回来?我们需要配合使用 defer(遗言/延迟执行)和 recover(复活/恢复)。这就像是给自己加了一个“复活甲”。
func ProtectRun() {
// 1. 提前定义好护体神功 (defer)
// 必须在 panic 发生之前定义
defer func() {
// 2. 尝试恢复 (recover)
if r := recover(); r != nil {
fmt.Println("🛑 捕获到崩溃,正在运功疗伤...")
fmt.Println("错误原因:", r)
}
}()
fmt.Println("正在正常运行...")
panic("啊!含笑半步癫发作了!") // 3. 发生崩溃
fmt.Println("这句依然不会执行")
}
func main() {
ProtectRun()
fmt.Println("呼~ 虽然受了内伤,但程序没有挂,还能继续运行!")
}
⚠️ 注意事项:
1. recover 只有在 defer 函数中调用才有效。
2. 只有在当前的 goroutine 中才能捕获 panic。
3. 不要滥用 panic/recover 来做普通的控制流(比如不要用它来当 try-catch 用),它只应该用于真正的异常情况。
下面的代码会因为除以零而崩溃。请使用 defer 和 recover 来捕获这个错误,防止程序退出。
package main
import "fmt"
func SafeDivide(a, b int) {
// 任务:在这里写 defer func() { ... }
defer func() {
if r := ______(); r != nil {
fmt.Println("捕获错误:", r)
}
}()
fmt.Println("结果是:", a / b)
}
func main() {
SafeDivide(10, 0)
fmt.Println("程序安全结束")
}
任务: 填空处的函数名是什么?(用于从恐慌中恢复)
答案: recover
解析: recover() 是一个内置函数,用于重新获得 panic 协程的控制权。它必须在 defer 函数中直接调用。