Go 错误处理进阶
目录
- Go 错误处理哲学
- error 接口本质
- 自定义错误类型
- fmt.Errorf 与 %w 包装错误
- errors.Is 和 errors.As
- 哨兵错误模式
- 错误处理最佳实践
- 实际项目中的错误处理模式
1. Go 错误处理哲学
1.1 与 try-catch 的根本区别
在 Java、Python、C++ 等语言中,异常处理依赖 try-catch 机制——异常被"抛出"后沿调用栈向上传播,直到被某个 catch 块捕获。这种模式有几个问题:
- 异常的传播路径是隐式的,调用者不知道某个函数可能抛出什么异常
- 异常控制流类似
goto,会打断正常的代码逻辑 - 容易被滥用(用异常做流程控制)
Go 采用了完全不同的哲学:错误是值(errors are values)。错误通过函数返回值显式传递,调用者必须主动检查并处理错误。
// Java 风格(隐式异常传播)
try {
File f = openFile("config.json");
String data = readAll(f);
Config config = parse(data);
} catch (FileNotFoundException e) {
// 处理文件不存在
} catch (IOException e) {
// 处理IO错误
} catch (ParseException e) {
// 处理解析错误
}
// Go 风格(显式错误处理)
f, err := os.Open("config.json")
if err != nil {
return fmt.Errorf("打开配置文件失败: %w", err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("读取配置文件失败: %w", err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return fmt.Errorf("解析配置文件失败: %w", err)
}
1.2 显式错误处理的优势
package main
import (
"fmt"
"strconv"
)
// 每个可能失败的操作都显式返回错误
// 调用者可以清楚看到哪些步骤可能出错
func parseAndDouble(s string) (int, error) {
// 第一步:解析字符串为整数
n, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("解析整数失败: %w", err)
}
// 第二步:检查范围
if n < 0 || n > 1000 {
return 0, fmt.Errorf("数值 %d 超出有效范围 [0, 1000]", n)
}
return n * 2, nil
}
func main() {
// 正常情况
result, err := parseAndDouble("42")
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Println("结果:", result) // 结果: 84
// 非数字
_, err = parseAndDouble("abc")
if err != nil {
fmt.Println("错误:", err) // 错误: 解析整数失败: strconv.Atoi: parsing "abc": invalid syntax
}
// 超出范围
_, err = parseAndDouble("9999")
if err != nil {
fmt.Println("错误:", err) // 错误: 数值 9999 超出有效范围 [0, 1000]
}
}
1.3 Go 的错误处理格言
Rob Pike 在 "Errors are values" 博文中指出,既然错误是普通的值,就可以用编程技巧来简化错误处理,而不是机械地 if err != nil。
package main
import (
"bufio"
"fmt"
"os"
)
// errWriter 封装了 writer 和错误状态
// 一旦发生错误,后续写入操作自动跳过
type errWriter struct {
w *bufio.Writer
err error
}
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return // 已经有错误了,跳过
}
_, ew.err = ew.w.Write(buf)
}
func (ew *errWriter) flush() error {
if ew.err != nil {
return ew.err
}
return ew.w.Flush()
}
func writeReport(f *os.File) error {
bw := bufio.NewWriter(f)
ew := &errWriter{w: bw}
// 不需要每次都检查错误,最后统一检查
ew.write([]byte("=== 报告标题 ===\n"))
ew.write([]byte("第一部分内容...\n"))
ew.write([]byte("第二部分内容...\n"))
ew.write([]byte("=== 报告结束 ===\n"))
return ew.flush()
}
func main() {
f, err := os.Create("/tmp/report.txt")
if err != nil {
fmt.Println("创建文件失败:", err)
return
}
defer f.Close()
if err := writeReport(f); err != nil {
fmt.Println("写入报告失败:", err)
return
}
fmt.Println("报告写入成功")
}
2. error 接口本质
2.1 error 是一个接口
Go 内置的 error 类型实际上是一个非常简单的接口:
// Go 标准库中的定义
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都满足 error 接口。这种设计极其简洁而强大。
2.2 标准库中的 errors.New 和 fmt.Errorf
package main
import (
"errors"
"fmt"
)
func main() {
// errors.New 创建一个简单的错误
err1 := errors.New("something went wrong")
fmt.Println(err1) // something went wrong
fmt.Printf("%T\n", err1) // *errors.errorString
// fmt.Errorf 创建格式化的错误
name := "config.json"
err2 := fmt.Errorf("无法打开文件 %s", name)
fmt.Println(err2) // 无法打开文件 config.json
// error 的零值是 nil,表示没有错误
var err3 error
fmt.Println(err3 == nil) // true
}
2.3 errors.New 的内部实现
// 标准库 errors 包的实现非常简单
// 这里展示其核心逻辑
package main
import "fmt"
// errorString 是一个实现了 error 接口的简单结构体
type errorString struct {
s string
}
// Error 方法使 errorString 满足 error 接口
func (e *errorString) Error() string {
return e.s
}
// New 创建一个新的错误,返回指针
// 返回指针而不是值,是为了让每次 New 调用产生不同的错误实例
func New(text string) error {
return &errorString{text}
}
func main() {
err1 := New("出错了")
err2 := New("出错了")
// 虽然文本相同,但它们是不同的错误实例(指针不同)
fmt.Println(err1 == err2) // false
fmt.Println(err1) // 出错了
}
2.4 nil error 的陷阱
package main
import "fmt"
// MyError 自定义错误类型
type MyError struct {
Code int
Message string
}
func (e *MyError) Error() string {
return fmt.Sprintf("错误码 %d: %s", e.Code, e.Message)
}
func doSomething(fail bool) error {
// 注意:这里声明了一个具体类型的指针
var err *MyError
if fail {
err = &MyError{Code: 404, Message: "未找到"}
}
// 危险!即使 err 是 nil 指针,返回后 error 接口值不为 nil
return err
}
func doSomethingCorrect(fail bool) error {
if fail {
return &MyError{Code: 404, Message: "未找到"}
}
// 直接返回 nil,不经过具体类型变量
return nil
}
func main() {
// 错误示范:即使没有失败,err 也不等于 nil!
err := doSomething(false)
if err != nil {
// 会进入这里!因为 error 接口内部持有 (*MyError, nil)
// 接口值只有在类型和值都为 nil 时才等于 nil
fmt.Println("错误的判断 - 进入了错误分支")
fmt.Printf("err 类型: %T, 值: %v\n", err, err)
}
// 正确示范
err2 := doSomethingCorrect(false)
if err2 == nil {
fmt.Println("正确:没有错误") // 正确:没有错误
}
}
关键教训:永远不要返回一个具体类型的 nil 指针作为 error 接口值。如果没有错误,直接
return nil。
3. 自定义错误类型
3.1 简单的自定义错误
package main
import "fmt"
// ValidationError 验证错误,包含字段名和错误原因
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("验证失败 [%s]: %s", e.Field, e.Message)
}
// NotFoundError 资源未找到错误
type NotFoundError struct {
Resource string
ID string
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s (ID: %s) 不存在", e.Resource, e.ID)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Message: "年龄不能为负数",
}
}
if age > 150 {
return &ValidationError{
Field: "age",
Message: "年龄不能超过150",
}
}
return nil
}
func findUser(id string) (string, error) {
// 模拟查找用户
users := map[string]string{
"1": "Alice",
"2": "Bob",
}
name, ok := users[id]
if !ok {
return "", &NotFoundError{
Resource: "用户",
ID: id,
}
}
return name, nil
}
func main() {
// 测试验证错误
if err := validateAge(-5); err != nil {
fmt.Println(err) // 验证失败 [age]: 年龄不能为负数
// 类型断言获取详细信息
if ve, ok := err.(*ValidationError); ok {
fmt.Printf("字段: %s, 原因: %s\n", ve.Field, ve.Message)
}
}
// 测试未找到错误
_, err := findUser("99")
if err != nil {
fmt.Println(err) // 用户 (ID: 99) 不存在
if nfe, ok := err.(*NotFoundError); ok {
fmt.Printf("资源类型: %s, ID: %s\n", nfe.Resource, nfe.ID)
}
}
}
3.2 携带上下文的错误类型
package main
import (
"fmt"
"time"
)
// OpError 记录操作上下文的错误
type OpError struct {
Op string // 操作名称
Path string // 资源路径
Err error // 底层错误(支持错误链)
Time time.Time // 发生时间
}
func (e *OpError) Error() string {
return fmt.Sprintf("操作 %s 在 %s 上失败 [%s]: %v",
e.Op, e.Path, e.Time.Format("15:04:05"), e.Err)
}
// Unwrap 支持错误链解包
func (e *OpError) Unwrap() error {
return e.Err
}
// PermissionError 权限错误
type PermissionError struct {
User string
Action string
}
func (e *PermissionError) Error() string {
return fmt.Sprintf("用户 %s 没有 %s 权限", e.User, e.Action)
}
func readConfig(user string) error {
// 模拟权限检查失败
permErr := &PermissionError{
User: user,
Action: "read",
}
// 包装为操作错误
return &OpError{
Op: "ReadConfig",
Path: "/etc/app/config.yaml",
Err: permErr,
Time: time.Now(),
}
}
func main() {
err := readConfig("guest")
if err != nil {
fmt.Println(err)
// 操作 ReadConfig 在 /etc/app/config.yaml 上失败 [14:30:05]: 用户 guest 没有 read 权限
}
}
3.3 多错误聚合
package main
import (
"fmt"
"strings"
)
// MultiError 聚合多个错误
type MultiError struct {
Errors []error
}
func (me *MultiError) Error() string {
if len(me.Errors) == 0 {
return "没有错误"
}
msgs := make([]string, len(me.Errors))
for i, err := range me.Errors {
msgs[i] = fmt.Sprintf(" [%d] %s", i+1, err.Error())
}
return fmt.Sprintf("发生 %d 个错误:\n%s", len(me.Errors), strings.Join(msgs, "\n"))
}
// Add 添加错误(忽略 nil)
func (me *MultiError) Add(err error) {
if err != nil {
me.Errors = append(me.Errors, err)
}
}
// ErrorOrNil 如果没有错误则返回 nil
func (me *MultiError) ErrorOrNil() error {
if len(me.Errors) == 0 {
return nil
}
return me
}
// validateUser 同时验证多个字段
func validateUser(name string, age int, email string) error {
me := &MultiError{}
if name == "" {
me.Add(fmt.Errorf("姓名不能为空"))
}
if age < 0 || age > 150 {
me.Add(fmt.Errorf("年龄 %d 不在有效范围内", age))
}
if !strings.Contains(email, "@") {
me.Add(fmt.Errorf("邮箱 %q 格式不正确", email))
}
return me.ErrorOrNil()
}
func main() {
// 多个字段验证失败
err := validateUser("", -1, "not-an-email")
if err != nil {
fmt.Println(err)
// 发生 3 个错误:
// [1] 姓名不能为空
// [2] 年龄 -1 不在有效范围内
// [3] 邮箱 "not-an-email" 格式不正确
}
// 全部验证通过
err = validateUser("Alice", 30, "alice@example.com")
fmt.Println("全部通过:", err == nil) // 全部通过: true
}
4. fmt.Errorf 与 %w 包装错误
4.1 错误包装基础(Go 1.13+)
Go 1.13 引入了错误包装机制,通过 fmt.Errorf 的 %w 动词可以将一个错误包装在另一个错误中,形成错误链。
package main
import (
"errors"
"fmt"
"os"
)
func readFile(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
// %w 包装原始错误,保留错误链
return nil, fmt.Errorf("读取文件 %s: %w", path, err)
}
return data, nil
}
func loadConfig() ([]byte, error) {
data, err := readFile("/etc/app/config.json")
if err != nil {
// 继续包装,形成多层错误链
return nil, fmt.Errorf("加载配置: %w", err)
}
return data, nil
}
func main() {
_, err := loadConfig()
if err != nil {
fmt.Println("错误信息:", err)
// 加载配置: 读取文件 /etc/app/config.json: open /etc/app/config.json: no such file or directory
// 可以通过 errors.Is 检查底层错误
if errors.Is(err, os.ErrNotExist) {
fmt.Println("根因:文件不存在")
}
}
}
4.2 %w 与 %v 的区别
package main
import (
"errors"
"fmt"
"io"
)
func example_w() error {
// %w 包装错误 -- 保留错误链,可以用 errors.Is/errors.As 查找
return fmt.Errorf("操作失败: %w", io.EOF)
}
func example_v() error {
// %v 仅格式化错误文本 -- 丢失错误链,无法用 errors.Is/errors.As 查找
return fmt.Errorf("操作失败: %v", io.EOF)
}
func main() {
err1 := example_w()
fmt.Println("使用 %%w:", errors.Is(err1, io.EOF)) // true -- 错误链完整
err2 := example_v()
fmt.Println("使用 %%v:", errors.Is(err2, io.EOF)) // false -- 错误链断裂
}
4.3 多重包装(Go 1.20+)
Go 1.20 起,fmt.Errorf 支持多个 %w,可以同时包装多个错误:
package main
import (
"errors"
"fmt"
"io"
"os"
)
func complexOperation() error {
err1 := os.ErrPermission
err2 := io.ErrUnexpectedEOF
// 同时包装两个错误
return fmt.Errorf("操作失败: %w 且 %w", err1, err2)
}
func main() {
err := complexOperation()
fmt.Println(err) // 操作失败: permission denied 且 unexpected EOF
// 两个底层错误都可以被 errors.Is 找到
fmt.Println("是权限错误?", errors.Is(err, os.ErrPermission)) // true
fmt.Println("是意外EOF?", errors.Is(err, io.ErrUnexpectedEOF)) // true
}
4.4 手动实现 Unwrap
package main
import (
"errors"
"fmt"
"io"
)
// QueryError 包装底层错误的自定义类型
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string {
return fmt.Sprintf("查询 %q 失败: %v", e.Query, e.Err)
}
// Unwrap 让 errors.Is/errors.As 能沿链查找
func (e *QueryError) Unwrap() error {
return e.Err
}
// JoinedError 包装多个底层错误(Go 1.20 风格)
type JoinedError struct {
Message string
Errs []error
}
func (e *JoinedError) Error() string {
return e.Message
}
// Unwrap 返回切片(Go 1.20+ 支持的多错误解包协议)
func (e *JoinedError) Unwrap() []error {
return e.Errs
}
func main() {
// 单错误链
qErr := &QueryError{
Query: "SELECT * FROM users",
Err: io.EOF,
}
fmt.Println(qErr) // 查询 "SELECT * FROM users" 失败: EOF
fmt.Println(errors.Is(qErr, io.EOF)) // true
// 多错误链
jErr := &JoinedError{
Message: "多个操作失败",
Errs: []error{io.EOF, io.ErrClosedPipe},
}
fmt.Println(errors.Is(jErr, io.EOF)) // true
fmt.Println(errors.Is(jErr, io.ErrClosedPipe)) // true
}
5. errors.Is 和 errors.As
5.1 errors.Is -- 检查错误链中是否包含某个特定错误值
package main
import (
"errors"
"fmt"
"io"
"os"
)
var ErrDatabase = errors.New("数据库错误")
func queryDB() error {
return fmt.Errorf("执行查询: %w", ErrDatabase)
}
func handleRequest() error {
return fmt.Errorf("处理请求: %w", queryDB())
}
func main() {
err := handleRequest()
// errors.Is 会沿着错误链逐层查找
// err -> "处理请求" -> "执行查询" -> ErrDatabase
fmt.Println(errors.Is(err, ErrDatabase)) // true
// 常见的标准库哨兵错误判断
err2 := fmt.Errorf("读取结束: %w", io.EOF)
fmt.Println(errors.Is(err2, io.EOF)) // true
// 不在错误链中的错误
fmt.Println(errors.Is(err, os.ErrNotExist)) // false
// == 只能匹配同一个错误实例,不能沿链查找
fmt.Println(err == ErrDatabase) // false(err 是包装后的,不等于原始值)
}
5.2 errors.As -- 从错误链中提取特定类型的错误
package main
import (
"errors"
"fmt"
"net"
)
// APIError 自定义API错误
type APIError struct {
StatusCode int
Message string
}
func (e *APIError) Error() string {
return fmt.Sprintf("API错误 %d: %s", e.StatusCode, e.Message)
}
func callAPI() error {
apiErr := &APIError{StatusCode: 403, Message: "禁止访问"}
return fmt.Errorf("调用用户服务: %w", apiErr)
}
func main() {
err := callAPI()
// errors.As 从错误链中找到匹配类型的错误并赋值给 target
var apiErr *APIError
if errors.As(err, &apiErr) {
fmt.Printf("状态码: %d, 消息: %s\n", apiErr.StatusCode, apiErr.Message)
// 状态码: 403, 消息: 禁止访问
}
// 标准库中的实际应用:提取网络错误的详细信息
_, netErr := net.Dial("tcp", "invalid-host:99999")
if netErr != nil {
var opErr *net.OpError
if errors.As(netErr, &opErr) {
fmt.Printf("网络操作: %s, 地址: %v\n", opErr.Op, opErr.Addr)
}
}
}
5.3 errors.Is 与 errors.As 的对比
package main
import (
"errors"
"fmt"
"os"
)
func main() {
// 制造一个文件操作错误
_, err := os.Open("/这个文件不存在")
if err == nil {
return
}
wrappedErr := fmt.Errorf("初始化失败: %w", err)
// errors.Is: 用于匹配特定的错误【值】
// 问题:这个错误是不是 os.ErrNotExist?
if errors.Is(wrappedErr, os.ErrNotExist) {
fmt.Println("Is: 文件不存在") // 匹配成功
}
// errors.As: 用于匹配特定的错误【类型】,并提取该类型的值
// 问题:这个错误链中有没有 *os.PathError 类型的错误?
var pathErr *os.PathError
if errors.As(wrappedErr, &pathErr) {
fmt.Printf("As: 路径=%s, 操作=%s\n", pathErr.Path, pathErr.Op)
// As: 路径=/这个文件不存在, 操作=open
}
// 总结:
// errors.Is(err, target) -> 值比较,类似 err == target 但支持错误链
// errors.As(err, &target) -> 类型匹配,类似类型断言但支持错误链
}
5.4 自定义 Is 和 As 方法
package main
import (
"errors"
"fmt"
)
// ErrCode 基于错误码的错误类型
type ErrCode struct {
Code int
Message string
}
func (e *ErrCode) Error() string {
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
// Is 自定义比较逻辑:只要错误码相同就认为匹配
func (e *ErrCode) Is(target error) bool {
t, ok := target.(*ErrCode)
if !ok {
return false
}
return e.Code == t.Code
}
func main() {
err1 := &ErrCode{Code: 404, Message: "用户未找到"}
err2 := &ErrCode{Code: 404, Message: "订单未找到"}
err3 := &ErrCode{Code: 500, Message: "内部错误"}
// 虽然消息不同,但错误码相同,Is 返回 true
fmt.Println(errors.Is(err1, err2)) // true
fmt.Println(errors.Is(err1, err3)) // false
// 包装后仍然有效
wrapped := fmt.Errorf("服务层: %w", err1)
target := &ErrCode{Code: 404} // 只关心错误码
fmt.Println(errors.Is(wrapped, target)) // true
}
6. 哨兵错误模式
6.1 什么是哨兵错误
哨兵错误(Sentinel Errors)是预定义的、包级别的错误变量,用作特定条件的标识符。调用者通过 errors.Is 来检查是否遇到了某个特定的已知错误。
package main
import (
"errors"
"fmt"
"io"
"bufio"
"strings"
)
func main() {
// 标准库中最常见的哨兵错误:io.EOF
reader := strings.NewReader("Hello")
scanner := bufio.NewScanner(reader)
// io.EOF 表示数据读取完毕,不是真正的"错误"
buf := make([]byte, 10)
for {
n, err := strings.NewReader("Hello").Read(buf)
if errors.Is(err, io.EOF) {
fmt.Printf("读取完毕,最后读取 %d 字节\n", n)
break
}
if err != nil {
fmt.Println("读取出错:", err)
break
}
fmt.Printf("读取 %d 字节: %s\n", n, buf[:n])
}
_ = scanner
}
6.2 标准库中的哨兵错误示例
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"os"
"time"
)
func main() {
// 1. io.EOF - 数据流结束
fmt.Println("io.EOF:", io.EOF)
// 2. io.ErrUnexpectedEOF - 读取到一半意外终止
fmt.Println("io.ErrUnexpectedEOF:", io.ErrUnexpectedEOF)
// 3. os.ErrNotExist - 文件不存在
_, err := os.Stat("/不存在的文件")
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在")
}
// 4. os.ErrPermission - 权限不足
fmt.Println("os.ErrPermission:", os.ErrPermission)
// 5. sql.ErrNoRows - 查询没有返回结果
// 注意:这不是错误,只是表示没有匹配的行
fmt.Println("sql.ErrNoRows:", sql.ErrNoRows)
// 6. context.Canceled - 上下文被取消
ctx, cancel := context.WithCancel(context.Background())
cancel()
fmt.Println("ctx.Err():", ctx.Err())
fmt.Println("是 Canceled?", errors.Is(ctx.Err(), context.Canceled))
// 7. context.DeadlineExceeded - 上下文超时
ctx2, cancel2 := context.WithTimeout(context.Background(), 1*time.Nanosecond)
defer cancel2()
time.Sleep(1 * time.Millisecond)
fmt.Println("是 DeadlineExceeded?", errors.Is(ctx2.Err(), context.DeadlineExceeded))
}
6.3 自定义哨兵错误
package main
import (
"errors"
"fmt"
)
// 包级别的哨兵错误,使用 errors.New 创建
var (
ErrUserNotFound = errors.New("用户未找到")
ErrUserExists = errors.New("用户已存在")
ErrInvalidPassword = errors.New("密码无效")
ErrAccountLocked = errors.New("账户已锁定")
)
// UserStore 模拟用户存储
type UserStore struct {
users map[string]string // username -> password
}
func NewUserStore() *UserStore {
return &UserStore{
users: map[string]string{
"alice": "secret123",
},
}
}
func (s *UserStore) Register(username, password string) error {
if _, exists := s.users[username]; exists {
return fmt.Errorf("注册用户 %s: %w", username, ErrUserExists)
}
s.users[username] = password
return nil
}
func (s *UserStore) Login(username, password string) error {
stored, exists := s.users[username]
if !exists {
return fmt.Errorf("登录 %s: %w", username, ErrUserNotFound)
}
if stored != password {
return fmt.Errorf("登录 %s: %w", username, ErrInvalidPassword)
}
return nil
}
func main() {
store := NewUserStore()
// 注册已存在的用户
err := store.Register("alice", "newpass")
if errors.Is(err, ErrUserExists) {
fmt.Println("注册失败:用户已存在")
}
// 登录不存在的用户
err = store.Login("bob", "pass")
if errors.Is(err, ErrUserNotFound) {
fmt.Println("登录失败:用户未找到")
}
// 密码错误
err = store.Login("alice", "wrong")
if errors.Is(err, ErrInvalidPassword) {
fmt.Println("登录失败:密码错误")
}
// 使用 switch 分发错误处理
err = store.Login("bob", "test")
switch {
case errors.Is(err, ErrUserNotFound):
fmt.Println("-> 建议注册新账号")
case errors.Is(err, ErrInvalidPassword):
fmt.Println("-> 建议重置密码")
case errors.Is(err, ErrAccountLocked):
fmt.Println("-> 请联系管理员")
case err != nil:
fmt.Println("-> 未知错误:", err)
default:
fmt.Println("-> 登录成功")
}
}
7. 错误处理最佳实践
7.1 何时使用 panic
panic 用于不可恢复的程序错误,而不是业务逻辑错误。
package main
import "fmt"
// 合理使用 panic 的场景
// 1. 程序初始化时的致命错误
func mustConnect(dsn string) {
// 如果数据库连接失败,程序无法运行
if dsn == "" {
panic("数据库连接字符串不能为空")
}
// ... 连接数据库
}
// 2. Must 函数模式:将 error 转化为 panic
// 标准库中有 template.Must, regexp.MustCompile 等
func MustParseConfig(path string) map[string]string {
config := map[string]string{"key": "value"} // 模拟解析
if path == "" {
panic("配置文件路径为空")
}
return config
}
// 3. 真正不可能发生的错误(程序逻辑bug)
func divide(a, b int) int {
if b == 0 {
// 如果调用者保证 b != 0,这里 panic 表示程序有 bug
panic("除数为零:这是一个编程错误")
}
return a / b
}
func main() {
// Must 模式通常在 init 或 main 的开头使用
config := MustParseConfig("app.yaml")
fmt.Println(config)
}
7.2 recover 捕获 panic
package main
import "fmt"
// safeExecute 安全执行一个函数,捕获可能的 panic
func safeExecute(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
// 将 panic 转化为 error
err = fmt.Errorf("捕获到 panic: %v", r)
}
}()
fn()
return nil
}
func riskyFunction() {
panic("出大事了!")
}
func main() {
// 场景:HTTP 中间件防止单个请求的 panic 导致服务崩溃
err := safeExecute(riskyFunction)
if err != nil {
fmt.Println("安全捕获:", err)
// 安全捕获: 捕获到 panic: 出大事了!
}
fmt.Println("程序继续运行...")
}
7.3 错误处理的黄金法则
package main
import (
"fmt"
"log"
)
// 规则1: 错误只应该被处理一次
// 要么处理它(记录日志、降级、返回默认值),要么向上传递
// 不要既记日志又返回错误
// 错误示范
func badExample() error {
err := fmt.Errorf("something failed")
if err != nil {
log.Println("错误:", err) // 处理了一次(记日志)
return err // 又返回了(让调用者再处理一次)
}
return nil
}
// 正确示范 A: 只向上传递(加上下文)
func goodExampleA() error {
err := fmt.Errorf("something failed")
if err != nil {
return fmt.Errorf("执行操作X: %w", err) // 只传递,不记日志
}
return nil
}
// 正确示范 B: 只在这里处理
func goodExampleB() int {
err := fmt.Errorf("something failed")
if err != nil {
log.Println("操作X失败,使用默认值:", err) // 处理:记日志+降级
return 42 // 返回默认值
}
return 0
}
// 规则2: 总是为错误添加上下文
func fetchUserOrders(userID string) error {
// 不好: return err
// 好: return fmt.Errorf("获取用户 %s 的订单: %w", userID, err)
return fmt.Errorf("获取用户 %s 的订单: %w", userID,
fmt.Errorf("数据库查询超时"))
}
// 规则3: 在顶层处理错误,底层只传递
func main() {
// 这里是"顶层",负责最终处理
if err := fetchUserOrders("u123"); err != nil {
log.Println("请求处理失败:", err)
// 可能还要返回 HTTP 500、发送告警等
}
_ = badExample()
_ = goodExampleA()
_ = goodExampleB()
}
7.4 不要忽略错误
package main
import (
"fmt"
"os"
)
func main() {
// 糟糕:忽略了 Close 的错误(写入可能没有刷到磁盘)
f, _ := os.Create("/tmp/test.txt")
f.Write([]byte("data"))
f.Close() // Close 可能返回错误!
// 正确方式
f2, err := os.Create("/tmp/test2.txt")
if err != nil {
fmt.Println("创建文件失败:", err)
return
}
_, err = f2.Write([]byte("data"))
if err != nil {
f2.Close()
fmt.Println("写入失败:", err)
return
}
if err := f2.Close(); err != nil {
fmt.Println("关闭文件失败(数据可能未完全写入):", err)
return
}
// 如果确实要忽略某个错误,用 _ 显式忽略并加注释
_ = os.Remove("/tmp/maybe-not-exists.txt") // 忽略: 文件可能不存在,不影响逻辑
}
8. 实际项目中的错误处理模式
8.1 分层错误设计
在一个分层架构中(Repository -> Service -> Handler),每层添加自己的上下文:
package main
import (
"errors"
"fmt"
)
// ========== 基础错误定义 ==========
var (
ErrNotFound = errors.New("资源未找到")
ErrUnauthorized = errors.New("未授权")
ErrInternal = errors.New("内部错误")
)
// ========== Repository 层 ==========
type UserRepo struct{}
func (r *UserRepo) FindByID(id int) (string, error) {
if id == 0 {
return "", fmt.Errorf("UserRepo.FindByID(id=%d): %w", id, ErrNotFound)
}
if id < 0 {
return "", fmt.Errorf("UserRepo.FindByID(id=%d): 数据库连接失败: %w", id, ErrInternal)
}
return "Alice", nil
}
// ========== Service 层 ==========
type UserService struct {
repo *UserRepo
}
func (s *UserService) GetUserProfile(id int) (string, error) {
name, err := s.repo.FindByID(id)
if err != nil {
return "", fmt.Errorf("UserService.GetUserProfile: %w", err)
}
return fmt.Sprintf("用户资料: %s", name), nil
}
// ========== Handler 层 ==========
type UserHandler struct {
service *UserService
}
func (h *UserHandler) HandleGetUser(id int) {
profile, err := h.service.GetUserProfile(id)
if err != nil {
// 在最顶层根据错误类型决定如何响应
switch {
case errors.Is(err, ErrNotFound):
fmt.Printf("HTTP 404: %v\n", err)
case errors.Is(err, ErrUnauthorized):
fmt.Printf("HTTP 401: %v\n", err)
case errors.Is(err, ErrInternal):
fmt.Printf("HTTP 500: %v\n", err)
default:
fmt.Printf("HTTP 500: 未知错误: %v\n", err)
}
return
}
fmt.Printf("HTTP 200: %s\n", profile)
}
func main() {
handler := &UserHandler{
service: &UserService{
repo: &UserRepo{},
},
}
handler.HandleGetUser(1) // HTTP 200: 用户资料: Alice
handler.HandleGetUser(0) // HTTP 404: UserService.GetUserProfile: UserRepo.FindByID(id=0): 资源未找到
handler.HandleGetUser(-1) // HTTP 500: UserService.GetUserProfile: UserRepo.FindByID(id=-1): 数据库连接失败: 内部错误
}
8.2 错误码设计模式
package main
import (
"errors"
"fmt"
)
// AppError 统一的应用错误类型
type AppError struct {
Code string // 业务错误码,如 "USER_001"
Message string // 面向用户的消息
Err error // 底层错误(可选)
}
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Err)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Err
}
// Is 允许通过错误码进行匹配
func (e *AppError) Is(target error) bool {
t, ok := target.(*AppError)
if !ok {
return false
}
return e.Code == t.Code
}
// HTTPStatus 根据错误码前缀返回 HTTP 状态码
func (e *AppError) HTTPStatus() int {
switch e.Code[:3] {
case "VAL": // 验证错误
return 400
case "AUT": // 认证错误
return 401
case "FBD": // 权限错误
return 403
case "NTF": // 未找到
return 404
default:
return 500
}
}
// 预定义的错误模板
var (
ErrCodeValidation = &AppError{Code: "VAL_001"}
ErrCodeNotFound = &AppError{Code: "NTF_001"}
ErrCodeAuth = &AppError{Code: "AUT_001"}
ErrCodeInternal = &AppError{Code: "INT_001"}
)
// NewNotFoundError 创建一个未找到错误
func NewNotFoundError(resource string, id interface{}) *AppError {
return &AppError{
Code: "NTF_001",
Message: fmt.Sprintf("%s (ID=%v) 不存在", resource, id),
}
}
// NewValidationError 创建一个验证错误
func NewValidationError(field, reason string) *AppError {
return &AppError{
Code: "VAL_001",
Message: fmt.Sprintf("字段 %s 验证失败: %s", field, reason),
}
}
// NewInternalError 创建一个内部错误(包装底层错误)
func NewInternalError(msg string, cause error) *AppError {
return &AppError{
Code: "INT_001",
Message: msg,
Err: cause,
}
}
func processOrder(userID, productID int) error {
if userID <= 0 {
return NewValidationError("userID", "必须大于0")
}
if productID == 99 {
return NewNotFoundError("商品", productID)
}
if productID < 0 {
return NewInternalError("查询商品失败",
fmt.Errorf("数据库超时"))
}
return nil
}
func handleRequest(userID, productID int) {
err := processOrder(userID, productID)
if err == nil {
fmt.Println("200 OK: 订单创建成功")
return
}
// 从错误中提取 AppError
var appErr *AppError
if errors.As(err, &appErr) {
fmt.Printf("HTTP %d | 错误码: %s | %s\n",
appErr.HTTPStatus(), appErr.Code, appErr.Message)
} else {
fmt.Println("HTTP 500 | 未知错误:", err)
}
}
func main() {
handleRequest(1, 1) // 200 OK: 订单创建成功
handleRequest(0, 1) // HTTP 400 | 错误码: VAL_001 | 字段 userID 验证失败: 必须大于0
handleRequest(1, 99) // HTTP 404 | 错误码: NTF_001 | 商品 (ID=99) 不存在
handleRequest(1, -1) // HTTP 500 | 错误码: INT_001 | 查询商品失败
// 使用 errors.Is 通过错误码模板匹配
err := processOrder(0, 1)
fmt.Println("\n是验证错误?", errors.Is(err, ErrCodeValidation)) // true
fmt.Println("是未找到?", errors.Is(err, ErrCodeNotFound)) // false
}
8.3 defer 与错误处理的结合
package main
import (
"fmt"
"os"
)
// writeToFile 演示使用 defer 和命名返回值处理关闭错误
func writeToFile(path, content string) (err error) {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("创建文件: %w", err)
}
// 使用 defer + 命名返回值来处理 Close 错误
defer func() {
closeErr := f.Close()
if err == nil {
// 如果写入没有出错,但 Close 出错了,返回 Close 的错误
err = closeErr
}
// 如果写入已经出错了,Close 的错误就忽略(保留原始错误)
}()
_, err = f.WriteString(content)
if err != nil {
return fmt.Errorf("写入内容: %w", err)
}
return nil
}
// transaction 模拟数据库事务的错误处理模式
func transaction(shouldFail bool) (err error) {
fmt.Println("开始事务")
defer func() {
if err != nil {
fmt.Println("回滚事务")
// rollback()
} else {
fmt.Println("提交事务")
// commit()
}
}()
fmt.Println("执行操作1")
if shouldFail {
return fmt.Errorf("操作1失败")
}
fmt.Println("执行操作2")
return nil
}
func main() {
// 写文件
if err := writeToFile("/tmp/test_defer.txt", "Hello, Go!"); err != nil {
fmt.Println("写文件失败:", err)
} else {
fmt.Println("写文件成功")
}
// 事务成功
fmt.Println("\n--- 事务成功 ---")
transaction(false)
// 事务失败
fmt.Println("\n--- 事务失败 ---")
transaction(true)
}
8.4 使用 errors.Join(Go 1.20+)
package main
import (
"errors"
"fmt"
"os"
)
// cleanup 清理多个资源,收集所有错误
func cleanup(files []*os.File) error {
var errs []error
for _, f := range files {
if err := f.Close(); err != nil {
errs = append(errs, fmt.Errorf("关闭 %s: %w", f.Name(), err))
}
}
// errors.Join 将多个错误合并为一个
// 如果 errs 为空,返回 nil
return errors.Join(errs...)
}
func main() {
// errors.Join 基本用法
err := errors.Join(
fmt.Errorf("错误1: 连接超时"),
fmt.Errorf("错误2: 写入失败"),
nil, // nil 会被自动忽略
fmt.Errorf("错误3: 缓存未命中"),
)
fmt.Println("合并后的错误:")
fmt.Println(err)
// 错误1: 连接超时
// 错误2: 写入失败
// 错误3: 缓存未命中
// errors.Join 返回的错误也支持 errors.Is
sentinel := errors.New("特殊错误")
joined := errors.Join(
fmt.Errorf("包装: %w", sentinel),
fmt.Errorf("另一个错误"),
)
fmt.Println("\n包含特殊错误?", errors.Is(joined, sentinel)) // true
}
总结
| 场景 | 推荐方式 |
|---|---|
| 函数可能失败 | 返回 (result, error) |
| 添加上下文 | fmt.Errorf("上下文: %w", err) |
| 检查特定错误值 | errors.Is(err, target) |
| 提取特定错误类型 | errors.As(err, &target) |
| 定义已知错误条件 | 包级别哨兵变量 var ErrXxx = errors.New(...) |
| 定义携带数据的错误 | 自定义结构体实现 error 接口 |
| 不可恢复的错误 | panic(仅限程序 bug 或初始化失败) |
| 合并多个错误 | errors.Join(Go 1.20+) |
| 一次包装多个错误 | fmt.Errorf("... %w ... %w", err1, err2)(Go 1.20+) |
主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/6719