Go: Go 错误处理与包管理

最后更新:2026-08-26

错误是值,不是异常——Go 把错误当作普通返回值处理,这种设计让错误处理变得显式、可控、可组合。

Go 的错误处理哲学和包管理是生产级代码的基石。这节课你将掌握 Go 最被低估的两个核心能力。

1. 你将学到


2. 一个微服务工程师的真实故事

(1) 痛点:线上 panic 导致服务崩溃,500 错误淹没了报警群

Alice 是微服务团队的后端工程师,她维护的用户服务最近出了大问题:

"用户服务上周挂了 3 次,每次都是因为一个 nil pointer dereference。Go 服务一 panic 整个进程就没了,所有用户都登不了——PM 说再挂一次就扣奖金。"

她打开故障现场的代码:

GO
// 坏代码:没有错误处理,直接 panic
func getUserByID(db *sql.DB, id int) *User {
    rows, _ := db.Query("SELECT * FROM users WHERE id = ?", id)
    // 如果 id 不存在,rows.Next() 返回 false
    // 但下面直接取值,rows.Scan 操作 nil -> panic
    var user User
    for rows.Next() {
        rows.Scan(&user.Name, &user.Age)
    }
    return &user
}

三个问题:(1) 忽略了 db.Query 的错误;(2) 没有检查结果是否存在;(3) 一旦 panic 整个进程崩溃。

(2) Go 的解法:错误是值

GO
// user_service.go
package main

import (
    "errors"
    "fmt"
)

// 自定义错误类型
type NotFoundError struct {
    ID int
}

func (e NotFoundError) Error() string {
    return fmt.Sprintf("user %d not found", e.ID)
}

// 错误哨兵
var ErrInvalidInput = errors.New("invalid input")

// 健壮的查询函数
func findUser(id int) (*User, error) {
    if id <= 0 {
        return nil, fmt.Errorf("findUser: %w", ErrInvalidInput)
    }

    users := map[int]User{
        1: {Name: "Alice", Age: 28},
        2: {Name: "Bob", Age: 32},
    }

    user, ok := users[id]
    if !ok {
        return nil, NotFoundError{ID: id}
    }
    return &user, nil
}

type User struct {
    Name string
    Age  int
}

func main() {
    for _, id := range []int{1, -1, 999} {
        user, err := findUser(id)
        if err != nil {
            // 判断错误类型
            if errors.Is(err, ErrInvalidInput) {
                fmt.Printf("输入错误(跳过):%v\n", err)
                continue
            }
            var nf NotFoundError
            if errors.As(err, &nf) {
                fmt.Printf("用户 %d 不存在\n", nf.ID)
                continue
            }
            fmt.Printf("未知错误:%v\n", err)
            continue
        }
        fmt.Printf("找到:%s (%d)\n", user.Name, user.Age)
    }
}

输出:

TEXT 📖 仅展示
找到:Alice (28)
输入错误(跳过):findUser: invalid input
用户 999 不存在

(3) 收益:错误处理对比

维度 try-catch 语言 Go 的 error
错误是 异常控制流 普通返回值
是否显式 隐式,容易漏 catch 显式 if err != nil
性能 有栈展开开销 无额外开销
组合性 差(异常打断流程) 好(err 可自由传递)
💡 提示: Java/C++/Python 的异常在抛出时要做栈展开(stack unwinding),Go 的 error 只是一个接口值(16 bytes),传递几乎无开销。


3. error interface

(1) error 是什么

GO
type error interface {
    Error() string
}

任何实现了 Error() string 方法的类型都是 error。

▶ 示例:4 种创建 error 的方式

GO
package main

import (
    "errors"
    "fmt"
)

// 方式 1:errors.New(最常用)
var ErrNotFound = errors.New("resource not found")

// 方式 2:fmt.Errorf(带格式化)
func validate(age int) error {
    if age < 0 {
        return fmt.Errorf("invalid age: %d (must be >= 0)", age)
    }
    return nil
}

// 方式 3:fmt.Errorf 包裹错误(%w)
func loadConfig(path string) error {
    if path == "" {
        return fmt.Errorf("loadConfig: %w", ErrNotFound)
    }
    return nil
}

// 方式 4:自定义 error 类型
type TimeoutError struct {
    DurationMs int
    Operation  string
}

func (e TimeoutError) Error() string {
    return fmt.Sprintf("%s timed out after %dms", e.Operation, e.DurationMs)
}

func main() {
    // 方式 1
    fmt.Println(ErrNotFound)  // resource not found

    // 方式 2
    fmt.Println(validate(-5))  // invalid age: -5 (must be >= 0)

    // 方式 3
    fmt.Println(loadConfig(""))  // loadConfig: resource not found

    // 方式 4
    err := TimeoutError{DurationMs: 5000, Operation: "DB query"}
    fmt.Println(err)  // DB query timed out after 5000ms
}
▶ 试一试

(2) 4 种创建方式对比

方式 函数/语法 用途 支持错误链?
errors.New errors.New("msg") 简单静态错误
fmt.Errorf fmt.Errorf("msg %d", n) 带格式化错误
fmt.Errorf(%w) fmt.Errorf("ctx: %w", err) 包裹错误 ✅ errors.Is/As
自定义类型 struct { ... Error() string } 带额外字段的错误 ✅ 自定义

4. errors.Is / errors.As 错误链

(1) errors.Is:检查错误链中是否包含某个哨兵

GO
package main

import (
    "errors"
    "fmt"
)

var ErrDB = errors.New("database error")
var ErrConn = fmt.Errorf("connection failed: %w", ErrDB)

func main() {
    err := fmt.Errorf("query failed: %w", ErrConn)

    // errors.Is 沿着 %w 链逐层查找
    fmt.Println(errors.Is(err, ErrDB))    // true
    fmt.Println(errors.Is(err, ErrConn))  // true

    // == 只能匹配最外层
    fmt.Println(err == ErrDB)   // false(不同对象)
    fmt.Println(err == ErrConn) // false
}

▶ 示例:errors.As:提取链中特定类型的错误

GO
package main

import (
    "errors"
    "fmt"
)

type ValidationError struct {
    Field string
    Value interface{}
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("validation failed: %s = %v", e.Field, e.Value)
}

func process(input string) error {
    if input == "" {
        return ValidationError{Field: "input", Value: ""}
    }
    return nil
}

func main() {
    err := process("")

    // errors.As 提取链中 ValidationError 类型
    var valErr ValidationError
    if errors.As(err, &valErr) {
        fmt.Printf("字段 %s 无效, 值=%v\n", valErr.Field, valErr.Value)
    }

    // 也可以带包裹
    wrapped := fmt.Errorf("process failed: %w", err)
    var valErr2 ValidationError
    if errors.As(wrapped, &valErr2) {
        fmt.Printf("(包裹后)字段 %s 无效\n", valErr2.Field)
    }
}
▶ 试一试

输出:

TEXT 📖 仅展示
字段 input 无效, 值=
(包裹后)字段 input 无效

(2) errors.Is vs errors.As

函数 匹配方式 用途
errors.Is(err, target) 值相等(==) 检查是否遇到特定哨兵错误
errors.As(err, &target) 类型匹配 提取链中某个类型的错误

5. panic / recover

(1) panic:不可恢复的错误

GO
package main

import "fmt"

func main() {
    fmt.Println("开始")

    // panic 会立刻停止当前函数,开始栈展开
    panic("something went terribly wrong")

    // 这行不会执行
    fmt.Println("结束")
}

输出:

TEXT 📖 仅展示
开始
panic: something went terribly wrong

goroutine 1 [running]:
main.main()
        /tmp/main.go:8 +0x...
exit status 2

▶ 示例:recover 恢复 panic

GO
package main

import (
    "fmt"
)

// recover 只在 defer 中有用
func safeDivide(a, b int) (result int, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("panic recovered: %v", r)
        }
    }()

    // 故意触发 panic
    if b == 0 {
        panic("division by zero")
    }
    return a / b, nil
}

func main() {
    // 正常调用
    if r, err := safeDivide(10, 2); err == nil {
        fmt.Printf("10/2 = %d\n", r)
    }

    // panic 被 recover 捕获,不会崩溃
    if r, err := safeDivide(10, 0); err != nil {
        fmt.Printf("Error: %v (result=%d)\n", err, r)
    }

    fmt.Println("程序正常结束——panic 被 recover 了")
}
▶ 试一试

输出:

TEXT 📖 仅展示
10/2 = 5
Error: panic recovered: division by zero (result=0)
程序正常结束——panic 被 recover 了

(2) panic vs error 使用场景

场景 用 error 用 panic
用户输入错误
文件不存在
网络超时
nil pointer dereference ❌(无法 recover) ✅(代码 bug)
数组越界 ❌(编译不检查) ✅(代码 bug)
初始化失败(必应条件)
🔥 易错: panic + recover 不应模拟 try-catch。Go 的哲学是"少用 panic,多用 error"。panic 只用于真正的异常情况(代码 bug、初始化失败、无法恢复的状态)。


6. go mod 包管理

(1) go mod 三大命令

命令 作用 常用时机
go mod init <module> 初始化模块 新项目开始
go mod tidy 清理依赖(加缺少的,删多余的) 改完 import 后
go mod add <path>@<ver> 添加依赖(Go 1.22+ 新增) 想加外部包
go get <path>@<ver> 添加/更新依赖 传统方式

▶ 示例:创建模块 + 添加依赖

BASH
# 1. 初始化模块
$ go mod init github.com/alice/user-service
go: creating new go.mod: module github.com/alice/user-service

# 2. 在代码中 import 外部包
GO
package main

import (
    "fmt"
    "github.com/google/uuid"  // 外部依赖
)

func main() {
    id := uuid.New()
    fmt.Printf("Generated UUID: %s\n", id)
}
BASH
# 3. 添加依赖并整理
$ go mod tidy
go: finding module for package github.com/google/uuid
go: found github.com/google/uuid in github.com/google/uuid v1.6.0

# 4. 查看生成的 go.mod
$ cat go.mod
module github.com/alice/user-service

go 1.22

require github.com/google/uuid v1.6.0

(2) 包导出规则

GO
// math/calculator.go
package math

// 大写开头 = 公开(可被其他包访问)
func Add(a, b int) int { return a + b }
var Version = "1.0"

// 小写开头 = 私有(仅包内可见)
func helper(x int) int { return x * 2 }
var internalVersion = "0.5"

// 公开结构体
type Calculator struct {
    // 公开字段
    Name string
    // 私有字段(包外不可直接访问)
    precision int
}
GO
package main

import "yourmodule/math"

func main() {
    math.Add(1, 2)      // ✅ 公开
    math.Version        // ✅ 公开变量

    // math.helper(5)   // ❌ 私有函数,编译错误
    // math.internalVersion  // ❌ 私有变量

    c := math.Calculator{Name: "basic"}  // ✅ 公开结构体
    // c.precision = 2  // ❌ 私有字段,编译错误
}

▶ 示例:包导出 + 错误类型传递

GO
// apperrors/errors.go
package apperrors

import "fmt"

// 公开错误类型(大写)
type BusinessError struct {
    Code    int
    Message string
}

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

// 公开哨兵
var ErrUnauthorized = BusinessError{Code: 401, Message: "unauthorized"}

// 私有错误(外部包无法直接引用)
type internalError struct {
    detail string
}

func (e internalError) Error() string {
    return fmt.Sprintf("internal: %s", e.detail)
}

// 公开工厂函数(外部包通过此函数间接使用 internalError)
func NewInternalError(detail string) error {
    return internalError{detail: detail}
}
▶ 试一试

7. 完整示例:健壮的用户服务

把错误处理、包管理、自定义错误全串起来:

GO
// user_service.go
package main

import (
    "errors"
    "fmt"
)

// ---------- 错误定义 ----------

type NotFoundError struct {
    Resource string
    ID       int
}

func (e NotFoundError) Error() string {
    return fmt.Sprintf("%s with id %d not found", e.Resource, e.ID)
}

type ValidationError struct {
    Field   string
    Message string
}

func (e ValidationError) Error() string {
    return fmt.Sprintf("validation failed: %s - %s", e.Field, e.Message)
}

type DBError struct {
    Operation string
    Err       error
}

func (e DBError) Error() string {
    return fmt.Sprintf("db %s failed: %v", e.Operation, e.Err)
}

func (e DBError) Unwrap() error {
    return e.Err
}

// 哨兵错误
var ErrInternal = errors.New("internal server error")

// ---------- 数据层(模拟 DB)----------

type User struct {
    ID   int
    Name string
    Age  int
}

func queryUserFromDB(id int) (*User, error) {
    db := map[int]User{
        1: {ID: 1, Name: "Alice", Age: 28},
        2: {ID: 2, Name: "Bob", Age: 32},
    }
    user, ok := db[id]
    if !ok {
        return nil, NotFoundError{Resource: "user", ID: id}
    }
    return &user, nil
}

// ---------- 服务层 ----------

func GetUser(id int) (*User, error) {
    // panic 保护
    defer func() {
        if r := recover(); r != nil {
            fmt.Printf("[PANIC] recovered: %v\n", r)
        }
    }()

    if id <= 0 {
        return nil, ValidationError{
            Field:   "id",
            Message: "must be positive",
        }
    }

    user, err := queryUserFromDB(id)
    if err != nil {
        var nf NotFoundError
        if errors.As(err, &nf) {
            return nil, nf
        }
        return nil, DBError{
            Operation: "queryUserFromDB",
            Err:       err,
        }
    }

    if user.Age < 0 || user.Age > 150 {
        return nil, ValidationError{
            Field:   "age",
            Message: fmt.Sprintf("unexpected age: %d", user.Age),
        }
    }

    return user, nil
}

// ---------- HTTP 层 ----------

func HandleGetUser(id int) {
    user, err := GetUser(id)
    if err != nil {
        var nf NotFoundError
        var ve ValidationError
        var de DBError

        switch {
        case errors.As(err, &nf):
            fmt.Printf("[404] %v\n", err)
        case errors.As(err, &ve):
            fmt.Printf("[400] %v\n", err)
        case errors.As(err, &de):
            fmt.Printf("[500] db error: %v\n", de)
            fmt.Printf("[500] 内部:%+v\n", de.Err)
        default:
            fmt.Printf("[500] %v\n", err)
        }
        return
    }
    fmt.Printf("[200] User: %+v\n", user)
}

func main() {
    // 正常
    HandleGetUser(1)

    // 输入错误(带额外信息的 ValidationError)
    HandleGetUser(0)

    // 用户不存在(自定义 NotFoundError)
    HandleGetUser(999)

    fmt.Println("\n=== 程序正常结束 ===")
}

预期输出:

TEXT 📖 仅展示
[200] User: &{ID:1 Name:Alice Age:28}
[400] validation failed: id - must be positive
[404] user with id 999 not found

=== 程序正常结束 ===
100%
flowchart TD
    A[函数返回 error] --> B{err == nil?}
    B -->|是| C[正常处理]
    B -->|否| D[判断错误类型]
    D --> E[errors.Is / == 哨兵]
    D --> F[errors.As / 类型断言]
    D --> G[类型 switch]
    E --> H[处理特定哨兵错误]
    F --> I[提取结构化错误信息]
    G --> J[按类型分支处理]
    H --> K[返回或重试]
    I --> K
    J --> K
🔥 易错: 第 57 行的 Unwrap() error 方法是让自定义错误参与错误链的关键。如果自定义类型没有 Unwrap() 方法,errors.Is / errors.As 就只会检查最外层。


❓ 常见问题

Q error 是什么类型?
A error 是一个内置接口:type error interface { Error() string }。任何实现了 Error() string 方法的类型都是 error——一个 16 字节的接口值。
Q 如何自定义错误?
A 定义一个 struct,实现 Error() string 方法。如果想支持错误链(errors.Is/As 穿透),再加一个 Unwrap() error 方法返回内层错误。
Q panic 后必须 recover 吗?
A 不一定。recover 只在 defer 中有用,且只应放在 goroutine 入口处(go func() { defer recover() })。不要在业务逻辑中用 recover——那是掩盖 bug,不是修 bug。
Q errors.Is 和 errors.As 有什么区别?
A errors.Is(err, target) 沿着 %w 链逐层做值比较(==);errors.As(err, &target) 沿着链逐层做类型断言,并填充 target。简单说:Is 检查值,As 提取类型。
Q go mod 怎么管理依赖?
A 核心流程:go mod init 初始化 → 写代码 import → go mod tidy 自动下载 + 整理 → go.mod + go.sum 锁定版本。Go 1.22+ 新增 go mod add 命令更直观。
Q 包外网名和私有的规则是什么?
A 一个规则:大写字母开头 = 公开(exported),小写 = 私有。适用于变量、函数、类型、结构体字段和常量。没有 public/private 关键字。
Q fmt.Errorf(%w) 和 fmt.Errorf(%v) 有什么区别?
A %w 创建带错误链的 error,可以被 errors.Is/As 穿透;%v 只格式化字符串,创建一个新 error,与原错误无关。
Q 在生产代码中如何优雅处理错误?
A (1) 层层包裹 fmt.Errorf("context: %w", err) 保持链; (2) 定义业务错误类型携带额外字段; (3) HTTP handler 层统一解析错误 → HTTP 状态码; (4) 日志记录完整链(%+v)。

📖 小节


📝 作业

  1. 基础题(难度⭐):定义 Divide 函数 func Divide(a, b float64) (float64, error),当除数为 0 时返回 errors.New("division by zero"),否则返回商。

  2. 进阶题(难度⭐⭐):实现一个 ConfigLoader:支持从 JSON 文件加载配置 + 环境变量回退。要求:用 fmt.Errorf(%w) 包裹每一层错误,调用者用 errors.Is 判断是"文件不存在"还是"JSON 解析错误"。

  3. 挑战题(难度⭐⭐⭐):构建一个三层错误处理架构:(1) 数据层 Repository → 返回 NotFoundError / DBError; (2) 服务层 Service → 包裹数据层错误 + 增加 ValidationError; (3) HTTP handler → 用 errors.As 逐层解析,映射到 HTTP 状态码(404/400/500)。要求结构体错误携带业务字段(ID/Field/Operation)。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏