Go: Go 错误处理与包管理
最后更新:2026-08-26
错误是值,不是异常——Go 把错误当作普通返回值处理,这种设计让错误处理变得显式、可控、可组合。
Go 的错误处理哲学和包管理是生产级代码的基石。这节课你将掌握 Go 最被低估的两个核心能力。
1. 你将学到
- error 接口与 4 种创建方式
errors.Is/errors.As判断错误链- 自定义错误类型
panic/recover使用场景go mod依赖管理(init/tidy/add)- 包导出规则(大写 = 公开,小写 = 私有)
- 用健壮错误处理构建用户服务
2. 一个微服务工程师的真实故事
(1) 痛点:线上 panic 导致服务崩溃,500 错误淹没了报警群
Alice 是微服务团队的后端工程师,她维护的用户服务最近出了大问题:
"用户服务上周挂了 3 次,每次都是因为一个 nil pointer dereference。Go 服务一 panic 整个进程就没了,所有用户都登不了——PM 说再挂一次就扣奖金。"
她打开故障现场的代码:
// 坏代码:没有错误处理,直接 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 的解法:错误是值
// 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)
}
}
输出:
找到:Alice (28)
输入错误(跳过):findUser: invalid input
用户 999 不存在
(3) 收益:错误处理对比
| 维度 | try-catch 语言 | Go 的 error |
|---|---|---|
| 错误是 | 异常控制流 | 普通返回值 |
| 是否显式 | 隐式,容易漏 catch | 显式 if err != nil |
| 性能 | 有栈展开开销 | 无额外开销 |
| 组合性 | 差(异常打断流程) | 好(err 可自由传递) |
3. error interface
(1) error 是什么
type error interface {
Error() string
}
任何实现了 Error() string 方法的类型都是 error。
▶ 示例:4 种创建 error 的方式
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:检查错误链中是否包含某个哨兵
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:提取链中特定类型的错误
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)
}
}
输出:
字段 input 无效, 值=
(包裹后)字段 input 无效
(2) errors.Is vs errors.As
| 函数 | 匹配方式 | 用途 |
|---|---|---|
errors.Is(err, target) |
值相等(==) | 检查是否遇到特定哨兵错误 |
errors.As(err, &target) |
类型匹配 | 提取链中某个类型的错误 |
5. panic / recover
(1) panic:不可恢复的错误
package main
import "fmt"
func main() {
fmt.Println("开始")
// panic 会立刻停止当前函数,开始栈展开
panic("something went terribly wrong")
// 这行不会执行
fmt.Println("结束")
}
输出:
开始
panic: something went terribly wrong
goroutine 1 [running]:
main.main()
/tmp/main.go:8 +0x...
exit status 2
▶ 示例:recover 恢复 panic
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 了")
}
输出:
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) |
| 初始化失败(必应条件) | ❌ | ✅ |
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> |
添加/更新依赖 | 传统方式 |
▶ 示例:创建模块 + 添加依赖
# 1. 初始化模块
$ go mod init github.com/alice/user-service
go: creating new go.mod: module github.com/alice/user-service
# 2. 在代码中 import 外部包
package main
import (
"fmt"
"github.com/google/uuid" // 外部依赖
)
func main() {
id := uuid.New()
fmt.Printf("Generated UUID: %s\n", id)
}
# 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) 包导出规则
// 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
}
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 // ❌ 私有字段,编译错误
}
▶ 示例:包导出 + 错误类型传递
// 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. 完整示例:健壮的用户服务
把错误处理、包管理、自定义错误全串起来:
// 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=== 程序正常结束 ===")
}
预期输出:
[200] User: &{ID:1 Name:Alice Age:28}
[400] validation failed: id - must be positive
[404] user with id 999 not found
=== 程序正常结束 ===
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
Unwrap() error 方法是让自定义错误参与错误链的关键。如果自定义类型没有 Unwrap() 方法,errors.Is / errors.As 就只会检查最外层。
❓ 常见问题
type error interface { Error() string }。任何实现了 Error() string 方法的类型都是 error——一个 16 字节的接口值。Error() string 方法。如果想支持错误链(errors.Is/As 穿透),再加一个 Unwrap() error 方法返回内层错误。go func() { defer recover() })。不要在业务逻辑中用 recover——那是掩盖 bug,不是修 bug。errors.Is(err, target) 沿着 %w 链逐层做值比较(==);errors.As(err, &target) 沿着链逐层做类型断言,并填充 target。简单说:Is 检查值,As 提取类型。go mod init 初始化 → 写代码 import → go mod tidy 自动下载 + 整理 → go.mod + go.sum 锁定版本。Go 1.22+ 新增 go mod add 命令更直观。public/private 关键字。%w 创建带错误链的 error,可以被 errors.Is/As 穿透;%v 只格式化字符串,创建一个新 error,与原错误无关。fmt.Errorf("context: %w", err) 保持链; (2) 定义业务错误类型携带额外字段; (3) HTTP handler 层统一解析错误 → HTTP 状态码; (4) 日志记录完整链(%+v)。📖 小节
- error 是内置接口,任何类型实现
Error() string就是 error - 4 种创建方式:
errors.New/fmt.Errorf/%w包裹 / 自定义类型 errors.Is检查错误链中是否包含目标(值匹配)errors.As从错误链中提取指定类型的错误(类型匹配)panic用于不可恢复的错误,recover只在 defer 中有效go mod init/tidy/add管理外部依赖- 包导出规则只有一条:大写=公开,小写=私有
- 生产代码:层层包裹错误 + 自定义错误类型 + 统一处理
📝 作业
-
基础题(难度⭐):定义
Divide函数func Divide(a, b float64) (float64, error),当除数为 0 时返回errors.New("division by zero"),否则返回商。 -
进阶题(难度⭐⭐):实现一个
ConfigLoader:支持从 JSON 文件加载配置 + 环境变量回退。要求:用fmt.Errorf(%w)包裹每一层错误,调用者用errors.Is判断是"文件不存在"还是"JSON 解析错误"。 -
挑战题(难度⭐⭐⭐):构建一个三层错误处理架构:(1) 数据层
Repository→ 返回NotFoundError/DBError; (2) 服务层Service→ 包裹数据层错误 + 增加 ValidationError; (3) HTTP handler → 用errors.As逐层解析,映射到 HTTP 状态码(404/400/500)。要求结构体错误携带业务字段(ID/Field/Operation)。