Go 错误处理机制详解:新手从 err != nil 到 errors.Is/As

刚开始学 Go 的时候,很多人都会被这段代码刷屏:

复制代码
if err != nil {
	return err
}

写多了以后,心里难免冒出一个问题:

复制代码
为什么 Go 到处都要手动判断 err?
为什么不像其他语言那样 try/catch?

这篇文章就从新手视角,把 Go 的错误处理机制讲清楚。

你会看到:

  • error 到底是什么

  • 为什么 Go 推荐显式处理错误

  • nil 在错误处理中是什么意思

  • 如何创建错误

  • 如何给错误添加上下文

  • 如何判断一个错误是不是某种错误

  • 如何取出自定义错误里的字段

  • panic 和普通错误有什么区别

  • 实战中怎样写出清晰的错误处理代码

先给一句核心结论:

复制代码
Go 把错误当作普通值处理。

错误不是隐藏的异常控制流,而是函数返回值的一部分。你看得见它,也必须决定怎么处理它。

一、Go 的 error 是什么

在 Go 里,error 是一个内置接口。

它可以理解成这样:

复制代码
type error interface {
	Error() string
}

只要一个类型实现了:

复制代码
Error() string

它就可以作为 error 使用。

最简单的例子:

复制代码
package main

import (
	"errors"
	"fmt"
)

func divide(a, b int) (int, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}

	return a / b, nil
}

func main() {
	result, err := divide(10, 0)
	if err != nil {
		fmt.Println("error:", err)
		return
	}

	fmt.Println("result:", result)
}

输出:

复制代码
error: division by zero

这里的函数返回两个值:

复制代码
func divide(a, b int) (int, error)

第一个值是正常结果。

第二个值是错误。

如果没有错误,返回:

复制代码
return result, nil

如果发生错误,返回:

复制代码
return zeroValue, err

这里的 zeroValue 是对应类型的零值,例如:

  • int 的零值是 0

  • string 的零值是 ""

  • bool 的零值是 false

  • 指针、slice、map、channel、interface 的零值是 nil

二、为什么是 if err != nil

Go 的错误处理通常长这样:

复制代码
value, err := doSomething()
if err != nil {
	return err
}

// 只有没有错误时,才继续使用 value。

这不是模板代码的装饰,而是在表达一个很明确的流程:

复制代码
调用函数
检查错误
如果有错误,处理或返回
如果没有错误,继续执行

Go 希望错误路径是显式的。

显式的好处是:

  • 你能清楚看到每一步可能失败

  • 你能在失败处补充上下文

  • 你能决定是重试、忽略、返回、记录日志,还是终止程序

  • 代码不会突然跳到远处的 catch 块

这也是 Go 风格里很重要的一点:

复制代码
错误处理是正常业务流程的一部分。

三、nil 表示没有错误

在 Go 里,nil 通常表示没有错误。

例如:

复制代码
func save(name string) error {
	if name == "" {
		return errors.New("name is empty")
	}

	// 保存成功,没有错误。
	return nil
}

调用方这样判断:

复制代码
err := save("alice")
if err != nil {
	fmt.Println("save failed:", err)
	return
}

fmt.Println("save success")

这就是 Go 里最常见的错误处理模式。

新手可以先记住:

复制代码
err == nil 表示成功
err != nil 表示失败

四、不要忽略错误

新手有时会这样写:

复制代码
result, _ := divide(10, 0)
fmt.Println(result)

这里的 _ 表示丢弃错误。

这在语法上可以,但在业务上通常很危险。

如果你忽略错误,就等于告诉程序:

复制代码
即使失败了,我也不关心。

但很多错误是必须处理的:

  • 文件不存在

  • 网络请求失败

  • JSON 解析失败

  • 数据库写入失败

  • 参数不合法

  • 权限不足

除非你非常确定这个错误可以忽略,否则不要随手写 _

更好的写法是:

复制代码
result, err := divide(10, 0)
if err != nil {
	fmt.Println("divide failed:", err)
	return
}

fmt.Println(result)

五、创建错误:errors.New

最简单的创建错误方式是 errors.New

复制代码
package main

import (
	"errors"
	"fmt"
)

func checkAge(age int) error {
	if age < 0 {
		return errors.New("age cannot be negative")
	}

	if age < 18 {
		return errors.New("age must be at least 18")
	}

	return nil
}

func main() {
	if err := checkAge(15); err != nil {
		fmt.Println("invalid age:", err)
		return
	}

	fmt.Println("age is valid")
}

输出:

复制代码
invalid age: age must be at least 18

errors.New 适合创建固定文本的错误。

如果错误里要带变量,就更常用 fmt.Errorf

六、创建带变量的错误:fmt.Errorf

fmt.Errorf 可以像 fmt.Sprintf 一样格式化错误信息。

复制代码
package main

import (
	"fmt"
)

func findUser(id int) error {
	if id <= 0 {
		return fmt.Errorf("invalid user id: %d", id)
	}

	return nil
}

func main() {
	if err := findUser(-1); err != nil {
		fmt.Println(err)
	}
}

输出:

复制代码
invalid user id: -1

相比:

复制代码
errors.New("invalid user id")

fmt.Errorf 可以把具体值放进去,让排查问题更方便。

七、错误信息应该怎么写

错误信息不是越长越好。

好的错误信息应该:

  • 说明哪里失败

  • 尽量带上关键上下文

  • 不要首字母大写

  • 不要以句号结尾

  • 不要写成用户界面的提示语

例如:

复制代码
return fmt.Errorf("open config %q: %w", path, err)

比下面这种更好:

复制代码
return fmt.Errorf("Error! Something went wrong.")

Go 里错误常常会被一层层包装,最后组成一句完整信息。

例如:

复制代码
load config "app.yaml": open file: permission denied

如果每一层都写成大写开头、感叹号、句号,最后就会很别扭。

八、添加上下文:不要只原样返回错误

假设你写了这样一个函数:

复制代码
func loadConfig(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return err
	}

	_ = data
	return nil
}

它能工作,但调用方看到错误时,可能只知道:

复制代码
open config.yaml: no such file or directory

如果项目里有很多地方都读文件,就不容易知道这次失败发生在哪个业务步骤。

更推荐给错误加上下文:

复制代码
func loadConfig(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("read config %q: %w", path, err)
	}

	_ = data
	return nil
}

注意这里用了 %w

复制代码
fmt.Errorf("read config %q: %w", path, err)

%w 表示包装一个错误。

包装以后:

  • 错误信息会带上上下文

  • 原始错误仍然可以被 errors.Iserrors.As 找到

完整例子:

复制代码
package main

import (
	"fmt"
	"os"
)

func loadConfig(path string) error {
	data, err := os.ReadFile(path)
	if err != nil {
		// %w 会包装原始错误,保留错误链。
		return fmt.Errorf("read config %q: %w", path, err)
	}

	fmt.Println("config size:", len(data))
	return nil
}

func main() {
	if err := loadConfig("missing.yaml"); err != nil {
		fmt.Println(err)
	}
}

可能输出:

复制代码
read config "missing.yaml": open missing.yaml: no such file or directory

这比单独返回底层错误更有用。

九、错误包装是什么

错误包装可以理解成:

复制代码
在原始错误外面套一层上下文。

例如:

复制代码
open missing.yaml: no such file or directory

被包装后变成:

复制代码
read config "missing.yaml": open missing.yaml: no such file or directory

如果再往上包装:

复制代码
start server: load app: read config "missing.yaml": open missing.yaml: no such file or directory

每一层都告诉你:

复制代码
我在做什么的时候失败了。

这对排查问题很重要。

十、errors.Is:判断错误是不是某个错误

有时你不只想打印错误,而是想判断错误类型。

例如:

复制代码
如果文件不存在,就创建默认配置。
如果是权限错误,就直接返回。

Go 推荐用 errors.Is 来判断错误链里是否包含某个目标错误。

复制代码
package main

import (
	"errors"
	"fmt"
	"os"
)

func loadConfig(path string) error {
	_, err := os.ReadFile(path)
	if err != nil {
		return fmt.Errorf("read config %q: %w", path, err)
	}

	return nil
}

func main() {
	err := loadConfig("missing.yaml")
	if err == nil {
		fmt.Println("config loaded")
		return
	}

	if errors.Is(err, os.ErrNotExist) {
		fmt.Println("config does not exist, use default config")
		return
	}

	fmt.Println("load config failed:", err)
}

输出:

复制代码
config does not exist, use default config

为什么不用字符串判断?

不要这样写:

复制代码
if strings.Contains(err.Error(), "no such file") {
	// ...
}

原因是:

  • 错误文本可能变化

  • 不同系统上的错误文本可能不同

  • 字符串匹配容易误判

  • 包装错误后文本更复杂

errors.Is 是结构化判断,比字符串判断可靠。

十一、哨兵错误 sentinel error

哨兵错误是预先定义好的固定错误值。

例如:

复制代码
var ErrNotFound = errors.New("not found")

调用方可以用 errors.Is 判断:

复制代码
if errors.Is(err, ErrNotFound) {
	// 处理未找到
}

完整例子:

复制代码
package main

import (
	"errors"
	"fmt"
)

var ErrUserNotFound = errors.New("user not found")

func findUserName(id int) (string, error) {
	if id == 100 {
		return "Alice", nil
	}

	return "", fmt.Errorf("find user %d: %w", id, ErrUserNotFound)
}

func main() {
	name, err := findUserName(200)
	if err != nil {
		if errors.Is(err, ErrUserNotFound) {
			fmt.Println("show empty user page")
			return
		}

		fmt.Println("find user failed:", err)
		return
	}

	fmt.Println("user:", name)
}

输出:

复制代码
show empty user page

哨兵错误适合表达稳定、可判断的错误状态。

常见例子:

  • io.EOF

  • os.ErrNotExist

  • context.Canceled

  • context.DeadlineExceeded

但不要给每一个错误都创建哨兵错误。

如果调用方不需要专门判断,就普通返回错误即可。

十二、errors.As:取出某种错误类型

errors.Is 用来判断"是不是某个错误"。

errors.As 用来判断"错误链里有没有某种错误类型",并把它取出来。

例如你定义了一个带字段的错误类型:

复制代码
type ValidationError struct {
	Field string
	Value string
}

func (e ValidationError) Error() string {
	return fmt.Sprintf("invalid %s: %q", e.Field, e.Value)
}

调用方可能不只想知道"验证失败",还想知道哪个字段失败。

完整例子:

复制代码
package main

import (
	"errors"
	"fmt"
)

type ValidationError struct {
	Field string
	Value string
}

func (e ValidationError) Error() string {
	return fmt.Sprintf("invalid %s: %q", e.Field, e.Value)
}

func validateName(name string) error {
	if name == "" {
		return ValidationError{
			Field: "name",
			Value: name,
		}
	}

	return nil
}

func createUser(name string) error {
	if err := validateName(name); err != nil {
		return fmt.Errorf("create user: %w", err)
	}

	return nil
}

func main() {
	err := createUser("")
	if err == nil {
		fmt.Println("user created")
		return
	}

	var validationErr ValidationError
	if errors.As(err, &validationErr) {
		fmt.Println("field:", validationErr.Field)
		fmt.Println("value:", validationErr.Value)
		return
	}

	fmt.Println("create user failed:", err)
}

输出:

复制代码
field: name
value:

注意 errors.As 的第二个参数:

复制代码
errors.As(err, &validationErr)

这里要传目标变量的地址。

十三、自定义错误类型

当错误不只是一个字符串,而是需要携带结构化信息时,可以定义自己的错误类型。

例如:

复制代码
package main

import (
	"fmt"
	"time"
)

type RateLimitError struct {
	RetryAfter time.Duration
}

func (e RateLimitError) Error() string {
	return fmt.Sprintf("rate limited, retry after %s", e.RetryAfter)
}

func callAPI() error {
	return RateLimitError{
		RetryAfter: 2 * time.Second,
	}
}

func main() {
	err := callAPI()
	if err != nil {
		fmt.Println(err)
	}
}

输出:

复制代码
rate limited, retry after 2s

自定义错误类型适合:

  • 需要暴露错误分类

  • 需要携带字段

  • 调用方需要根据字段做不同处理

如果只是简单描述失败原因,errors.Newfmt.Errorf 就够了。

十四、errors.Join:合并多个错误

有时一个操作可能同时产生多个错误。

例如关闭多个资源时,可能每个资源都关闭失败。

Go 的 errors.Join 可以把多个错误合并成一个错误。

复制代码
package main

import (
	"errors"
	"fmt"
)

func main() {
	err1 := errors.New("close file failed")
	err2 := errors.New("close network failed")

	err := errors.Join(err1, err2)
	if err != nil {
		fmt.Println(err)
	}
}

可能输出:

复制代码
close file failed
close network failed

errors.Join 会忽略 nil 错误。

如果传进去的错误全是 nil,它会返回 nil。

示例:

复制代码
package main

import (
	"errors"
	"fmt"
)

func main() {
	err := errors.Join(nil, nil)
	fmt.Println(err == nil)
}

输出:

复制代码
true

在新手阶段,你不一定经常用到 errors.Join,但知道它可以表达"多个错误同时存在"就够了。

十五、defer 和错误处理

错误处理经常和 defer 一起出现。

defer 用来注册函数结束前要执行的操作,常见于释放资源。

例如:

复制代码
package main

import (
	"fmt"
	"os"
)

func readFile(path string) error {
	file, err := os.Open(path)
	if err != nil {
		return fmt.Errorf("open file %q: %w", path, err)
	}
	defer file.Close()

	buffer := make([]byte, 16)
	_, err = file.Read(buffer)
	if err != nil {
		return fmt.Errorf("read file %q: %w", path, err)
	}

	return nil
}

func main() {
	tmp, err := os.CreateTemp("", "go-error-demo-*.txt")
	if err != nil {
		fmt.Println("create temp file:", err)
		return
	}
	defer os.Remove(tmp.Name())

	if _, err := tmp.WriteString("hello go"); err != nil {
		fmt.Println("write temp file:", err)
		return
	}
	tmp.Close()

	if err := readFile(tmp.Name()); err != nil {
		fmt.Println("read failed:", err)
		return
	}

	fmt.Println("read success")
}

输出:

复制代码
read success

defer file.Close() 的意思是:

复制代码
不管 readFile 后面是成功返回,还是因为错误提前返回,都要关闭文件。

十六、关闭资源时的错误要不要处理

很多人会写:

复制代码
defer file.Close()

这很常见,但有一个细节:

复制代码
Close 本身也可能返回错误。

如果你写的是只读文件,忽略 Close 错误通常问题不大。

但如果你写文件,Close 时可能才发现刷盘失败。这时最好处理关闭错误。

示例:

复制代码
package main

import (
	"fmt"
	"os"
)

func writeReport(path string, content string) (err error) {
	file, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("create report %q: %w", path, err)
	}

	defer func() {
		closeErr := file.Close()
		if closeErr != nil && err == nil {
			err = fmt.Errorf("close report %q: %w", path, closeErr)
		}
	}()

	if _, err := file.WriteString(content); err != nil {
		return fmt.Errorf("write report %q: %w", path, err)
	}

	return nil
}

func main() {
	tmp, err := os.CreateTemp("", "report-*.txt")
	if err != nil {
		fmt.Println("create temp file:", err)
		return
	}
	path := tmp.Name()
	tmp.Close()
	defer os.Remove(path)

	if err := writeReport(path, "hello report"); err != nil {
		fmt.Println("write report failed:", err)
		return
	}

	fmt.Println("write report success")
}

这里用了命名返回值:

复制代码
func writeReport(path string, content string) (err error)

defer 里可以看到即将返回的 err,并在需要时补上关闭错误。

这个写法对新手来说稍微绕一点。先理解思路就好:

复制代码
如果 Close 的错误很重要,就不要完全忽略它。

十七、panic 不是普通错误处理

Go 里还有 panic

panic 会停止当前函数的正常执行,并开始展开调用栈。已经注册的 defer 会执行。

示例:

复制代码
package main

import "fmt"

func main() {
	defer fmt.Println("defer runs")

	fmt.Println("before panic")
	panic("something is broken")
}

输出大致会是:

复制代码
before panic
defer runs
panic: something is broken

程序会异常退出。

那什么时候用 panic

新手可以先记住:

复制代码
普通可预期错误用 error。
程序无法继续、违反内部不变量时,才考虑 panic。

例如:

  • 用户输入错误:返回 error

  • 文件不存在:返回 error

  • 网络超时:返回 error

  • 配置格式错误:返回 error

  • 数组越界、空指针、不可恢复的内部状态:可能触发 panic

不要把 panic 当成 try/catch 的替代品。

十八、recover:从 panic 中恢复

recover 可以在 defer 函数中捕获 panic,让程序恢复控制。

示例:

复制代码
package main

import "fmt"

func safeRun() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("recovered:", r)
		}
	}()

	fmt.Println("before panic")
	panic("boom")
}

func main() {
	safeRun()
	fmt.Println("program continues")
}

输出:

复制代码
before panic
recovered: boom
program continues

recover 只能在 deferred function 里直接调用才有效。

但是注意:

复制代码
recover 不是让你随便吞掉所有 panic。

更常见的使用场景是:

  • HTTP 服务器中间件兜底,避免单个请求导致整个服务退出

  • goroutine 顶层保护,记录 panic 日志

  • 框架边界把 panic 转成错误响应

业务逻辑里的普通失败,仍然应该返回 error

十九、把 panic 转成 error

有时你调用的代码可能 panic,但你希望函数对外返回 error

可以这样写:

复制代码
package main

import (
	"fmt"
)

func riskyDivide(a, b int) int {
	if b == 0 {
		panic("division by zero")
	}
	return a / b
}

func safeDivide(a, b int) (result int, err error) {
	defer func() {
		if r := recover(); r != nil {
			err = fmt.Errorf("safe divide failed: %v", r)
		}
	}()

	result = riskyDivide(a, b)
	return result, nil
}

func main() {
	result, err := safeDivide(10, 0)
	if err != nil {
		fmt.Println(err)
		return
	}

	fmt.Println(result)
}

输出:

复制代码
safe divide failed: division by zero

这类写法应该放在边界位置,不应该让整个项目都依赖 panic/recover 做普通流程控制。

二十、错误应该在哪里处理

当一个函数返回错误时,调用方有几种选择:

1. 直接处理

例如文件不存在时使用默认配置:

复制代码
if errors.Is(err, os.ErrNotExist) {
	useDefaultConfig()
	return nil
}

2. 添加上下文后继续返回

例如底层读文件失败,上层说明自己正在加载配置:

复制代码
return fmt.Errorf("load config: %w", err)

3. 转换成业务错误

例如数据库没找到用户,转换成业务层的 ErrUserNotFound

复制代码
return fmt.Errorf("get profile: %w", ErrUserNotFound)

4. 记录日志并终止流程

例如 main 函数里:

复制代码
if err := run(); err != nil {
	log.Fatal(err)
}

新手最容易犯的错误是:每一层都打印日志,然后又继续返回错误。

例如:

复制代码
if err != nil {
	log.Println(err)
	return err
}

如果很多层都这么写,最后日志里会重复出现一堆相似错误。

更清楚的做法通常是:

复制代码
底层返回错误
中间层添加上下文
最外层统一记录日志

二十一、实战例子:读取配置并处理错误

下面写一个稍微完整的例子。

需求:

复制代码
读取配置文件。
如果文件不存在,使用默认配置。
如果文件为空,返回验证错误。
如果读取失败,保留底层错误。

完整代码:

复制代码
package main

import (
	"errors"
	"fmt"
	"os"
	"strings"
)

var ErrEmptyConfig = errors.New("empty config")

type Config struct {
	AppName string
}

func parseConfig(content string) (Config, error) {
	content = strings.TrimSpace(content)
	if content == "" {
		return Config{}, ErrEmptyConfig
	}

	return Config{AppName: content}, nil
}

func loadConfig(path string) (Config, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return Config{}, fmt.Errorf("read config %q: %w", path, err)
	}

	config, err := parseConfig(string(data))
	if err != nil {
		return Config{}, fmt.Errorf("parse config %q: %w", path, err)
	}

	return config, nil
}

func defaultConfig() Config {
	return Config{AppName: "demo-app"}
}

func main() {
	config, err := loadConfig("missing.conf")
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			config = defaultConfig()
			fmt.Println("config file missing, use default config")
			fmt.Println("app name:", config.AppName)
			return
		}

		if errors.Is(err, ErrEmptyConfig) {
			fmt.Println("config file is empty")
			return
		}

		fmt.Println("load config failed:", err)
		return
	}

	fmt.Println("app name:", config.AppName)
}

输出:

复制代码
config file missing, use default config
app name: demo-app

这个例子里有几个关键点:

  1. parseConfig 只负责解析,不负责读文件。

  2. loadConfig 给读文件和解析错误加上下文。

  3. main 决定怎么处理不同错误。

  4. 判断错误时用 errors.Is,而不是字符串匹配。

  5. 文件不存在是可恢复错误,所以使用默认配置。

这就是比较典型的 Go 错误处理风格。

二十二、常见错误处理模式

早返回

Go 里很常见的写法是:

复制代码
if err != nil {
	return err
}

这叫早返回。

好处是:错误路径先处理,正常路径不用包在很深的 else 里。

不要写成:

复制代码
if err == nil {
	// 一大段正常逻辑
} else {
	return err
}

更推荐:

复制代码
if err != nil {
	return err
}

// 一大段正常逻辑

包装后返回

跨函数返回错误时,给错误加上下文:

复制代码
if err != nil {
	return fmt.Errorf("save user %d: %w", userID, err)
}

判断特殊错误

使用 errors.Is

复制代码
if errors.Is(err, os.ErrNotExist) {
	// 文件不存在
}

提取错误类型

使用 errors.As

复制代码
var pathErr *os.PathError
if errors.As(err, &pathErr) {
	fmt.Println("operation:", pathErr.Op)
	fmt.Println("path:", pathErr.Path)
}

二十三、常见误区

误区一:用 panic 处理普通错误

错误写法:

复制代码
if err != nil {
	panic(err)
}

如果这是普通文件读取、网络请求、参数校验,就不应该 panic。

更好的写法:

复制代码
if err != nil {
	return fmt.Errorf("read input: %w", err)
}

误区二:错误信息没有上下文

不太好:

复制代码
return err

更好:

复制代码
return fmt.Errorf("load user profile: %w", err)

当然也不是每一层都必须包装。关键是让最终错误信息能说明失败路径。

误区三:用字符串判断错误

不推荐:

复制代码
if err.Error() == "not found" {
	// ...
}

更推荐:

复制代码
if errors.Is(err, ErrNotFound) {
	// ...
}

误区四:吞掉错误

不推荐:

复制代码
doSomething()

如果函数返回错误,应该接住:

复制代码
if err := doSomething(); err != nil {
	return err
}

误区五:重复打印同一个错误

底层打印一次,中间层打印一次,最外层又打印一次,会让日志变乱。

通常只在边界层记录日志,例如:

  • main

  • HTTP handler

  • goroutine 顶层

  • CLI 命令入口

中间层优先返回带上下文的错误。

二十四、新手错误处理检查清单

写 Go 代码时,可以用这张清单检查:

  • 函数会失败吗?如果会,是否返回 error

  • 调用函数后,是否检查了 err != nil

  • 返回错误时,是否保留了原始错误?

  • 需要跨层传递时,是否用 %w 包装?

  • 判断错误时,是否使用 errors.Iserrors.As

  • 是否避免了用字符串匹配错误?

  • 是否只在真正异常的情况下使用 panic

  • 打开文件、连接等资源后,是否用 defer 释放?

  • 关闭资源的错误是否重要?如果重要,是否处理了?

  • 日志是否只在合适的边界层打印?

二十五、学习路线建议

如果你是新手,可以按这个顺序练:

  1. 写一个返回 error 的函数。

  2. errors.New 创建固定错误。

  3. fmt.Errorf 创建带变量的错误。

  4. if err != nil 做早返回。

  5. %w 包装错误。

  6. errors.Is 判断哨兵错误。

  7. 定义一个自定义错误类型。

  8. errors.As 取出自定义错误。

  9. defer 释放资源。

  10. 理解 panic/recover,但不要滥用。

这些练顺以后,Go 的错误处理就不再只是"满屏 if err != nil",而是一套很清晰的失败处理机制。

总结

Go 的错误处理可以压缩成几句话:

  • error 是一个接口,核心方法是 Error() string

  • nil 表示没有错误。

  • 普通失败应该返回 error,不要用 panic

  • errors.New 创建固定错误。

  • fmt.Errorf 创建格式化错误。

  • %w 用来包装错误,保留错误链。

  • errors.Is 用来判断错误链里是否有某个错误。

  • errors.As 用来取出错误链里的某种错误类型。

  • errors.Join 可以合并多个错误。

  • defer 常用于释放资源。

  • recover 只适合在边界位置处理 panic。

  • 底层返回错误,中间层加上下文,边界层统一记录日志。

最后记住一句:

复制代码
Go 的错误处理不是为了少写代码,而是为了让失败路径清楚可见。

当你能清楚回答"这个错误在哪里产生、在哪里补充上下文、在哪里被处理",你就真正开始掌握 Go 的错误处理了。

参考资料