错误处理与异常
2026/3/20大约 11 分钟
错误处理与异常
一、Go 错误处理哲学
Go 语言采用显式错误处理机制,而非传统的 try-catch 异常处理。这种设计强调:
Go 错误处理 vs 传统异常处理
├── 传统异常处理(Java/Python/C++)
│ ├── try-catch-finally 块
│ ├── 异常可以跨层传播
│ ├── 容易忽略错误处理
│ └── 性能开销(栈展开)
│
└── Go 错误处理
├── 错误作为返回值
├── 调用方必须显式处理
├── 代码逻辑清晰
└── panic/recover 仅用于真正的异常
二、error 接口
2.1 error 基础
package main
import (
"errors"
"fmt"
"os"
"strconv"
)
func main() {
// ========== 创建错误 ==========
// 方式一:errors.New
err1 := errors.New("something went wrong")
// 方式二:fmt.Errorf(支持格式化)
name := "config.yaml"
err2 := fmt.Errorf("failed to open file: %s", name)
fmt.Println(err1)
fmt.Println(err2)
// ========== 错误检查 ==========
// 标准模式
result, err := divide(10, 0)
if err != nil {
fmt.Println("错误:", err)
} else {
fmt.Println("结果:", result)
}
// 短变量声明模式
if f, err := os.Open("/nonexistent"); err != nil {
fmt.Println("打开文件失败:", err)
} else {
defer f.Close()
// 使用文件...
}
// ========== 错误类型判断 ==========
_, err = strconv.Atoi("not a number")
if err != nil {
// 类型断言检查具体错误类型
if numErr, ok := err.(*strconv.NumError); ok {
fmt.Printf("转换错误: Func=%s, Num=%s, Err=%v\n",
numErr.Func, numErr.Num, numErr.Err)
}
}
}
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
2.2 error 接口定义
package main
import "fmt"
// error 接口非常简单
// type error interface {
// Error() string
// }
// 任何实现了 Error() string 方法的类型都是 error
// 自定义错误类型
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on field '%s': %s", e.Field, e.Message)
}
// 运维场景:服务器错误
type ServerError struct {
Host string
Port int
Code int
Message string
}
func (e *ServerError) Error() string {
return fmt.Sprintf("[%s:%d] error %d: %s", e.Host, e.Port, e.Code, e.Message)
}
func (e *ServerError) IsRetryable() bool {
// 5xx 错误可重试
return e.Code >= 500 && e.Code < 600
}
func connectToServer(host string, port int) error {
// 模拟连接失败
return &ServerError{
Host: host,
Port: port,
Code: 503,
Message: "Service Unavailable",
}
}
func validateConfig(config map[string]string) error {
if config["host"] == "" {
return &ValidationError{Field: "host", Message: "cannot be empty"}
}
if config["port"] == "" {
return &ValidationError{Field: "port", Message: "cannot be empty"}
}
return nil
}
func main() {
// 服务器错误
err := connectToServer("10.0.0.1", 8080)
if err != nil {
fmt.Println("连接失败:", err)
// 类型断言获取详细信息
if serverErr, ok := err.(*ServerError); ok {
fmt.Printf("错误码: %d\n", serverErr.Code)
fmt.Printf("可重试: %v\n", serverErr.IsRetryable())
}
}
// 验证错误
config := map[string]string{"port": "8080"}
if err := validateConfig(config); err != nil {
fmt.Println("配置验证失败:", err)
if valErr, ok := err.(*ValidationError); ok {
fmt.Printf("问题字段: %s\n", valErr.Field)
}
}
}
三、错误包装(Go 1.13+)
3.1 错误包装与解包
package main
import (
"errors"
"fmt"
"os"
)
// 哨兵错误(预定义的错误值)
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized access")
ErrTimeout = errors.New("operation timeout")
ErrDatabase = errors.New("database error")
)
// 使用 %w 包装错误
func readConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
// %w 保留原始错误,可以用 errors.Is/As 检查
return nil, fmt.Errorf("failed to read config file %s: %w", path, err)
}
return data, nil
}
func loadAppConfig() ([]byte, error) {
data, err := readConfig("/etc/app/config.yaml")
if err != nil {
// 继续包装,形成错误链
return nil, fmt.Errorf("load app config: %w", err)
}
return data, nil
}
// 模拟数据库操作
func queryDatabase(query string) ([]string, error) {
// 模拟错误
return nil, fmt.Errorf("query failed: %w", ErrDatabase)
}
func getUserByID(id int) (string, error) {
results, err := queryDatabase(fmt.Sprintf("SELECT * FROM users WHERE id = %d", id))
if err != nil {
if errors.Is(err, ErrDatabase) {
return "", fmt.Errorf("get user %d: %w", id, ErrNotFound)
}
return "", fmt.Errorf("get user %d: %w", id, err)
}
if len(results) == 0 {
return "", fmt.Errorf("user %d: %w", id, ErrNotFound)
}
return results[0], nil
}
func main() {
// ========== errors.Is:检查错误链中是否包含特定错误 ==========
_, err := loadAppConfig()
if err != nil {
fmt.Println("错误:", err)
// 检查是否是文件不存在错误
if errors.Is(err, os.ErrNotExist) {
fmt.Println("配置文件不存在")
}
}
// ========== errors.As:从错误链中提取特定类型 ==========
_, err = getUserByID(999)
if err != nil {
fmt.Println("\n用户查询错误:", err)
// 检查是否包含 ErrNotFound
if errors.Is(err, ErrNotFound) {
fmt.Println("用户不存在")
}
// 提取 PathError 类型
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Printf("路径错误: Op=%s, Path=%s\n", pathErr.Op, pathErr.Path)
}
}
// ========== errors.Unwrap:获取被包装的错误 ==========
wrappedErr := fmt.Errorf("outer: %w", fmt.Errorf("inner: %w", ErrTimeout))
fmt.Println("\n包装错误:", wrappedErr)
fmt.Println("解包一层:", errors.Unwrap(wrappedErr))
// 完整解包
current := wrappedErr
for current != nil {
fmt.Println(" ->", current)
current = errors.Unwrap(current)
}
}
3.2 多错误处理(Go 1.20+)
package main
import (
"errors"
"fmt"
)
// Go 1.20+ 支持 errors.Join 合并多个错误
func validateServer(host string, port int, timeout int) error {
var errs []error
if host == "" {
errs = append(errs, errors.New("host cannot be empty"))
}
if port <= 0 || port > 65535 {
errs = append(errs, fmt.Errorf("invalid port: %d", port))
}
if timeout <= 0 {
errs = append(errs, errors.New("timeout must be positive"))
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
// 运维场景:并行检查多个服务器
var ErrHealthCheck = errors.New("health check failed")
func checkServers(servers []string) error {
var errs []error
for _, server := range servers {
if err := checkServer(server); err != nil {
errs = append(errs, fmt.Errorf("%s: %w", server, err))
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
func checkServer(server string) error {
// 模拟某些服务器检查失败
if server == "web-02" || server == "web-04" {
return ErrHealthCheck
}
return nil
}
func main() {
// 验证错误
err := validateServer("", -1, 0)
if err != nil {
fmt.Println("验证失败:")
fmt.Println(err)
fmt.Println()
}
// 批量检查
servers := []string{"web-01", "web-02", "web-03", "web-04", "web-05"}
err = checkServers(servers)
if err != nil {
fmt.Println("服务器检查失败:")
fmt.Println(err)
// 检查是否包含特定错误
if errors.Is(err, ErrHealthCheck) {
fmt.Println("\n存在健康检查失败的服务器")
}
}
}
四、自定义错误类型
4.1 错误分类与编码
package main
import (
"encoding/json"
"fmt"
"net/http"
)
// 错误码定义
type ErrorCode int
const (
ErrCodeUnknown ErrorCode = iota + 1000
ErrCodeValidation
ErrCodeNotFound
ErrCodeUnauthorized
ErrCodeForbidden
ErrCodeTimeout
ErrCodeDatabase
ErrCodeNetwork
ErrCodeInternal
)
// 错误码到 HTTP 状态码的映射
func (c ErrorCode) HTTPStatus() int {
switch c {
case ErrCodeValidation:
return http.StatusBadRequest
case ErrCodeNotFound:
return http.StatusNotFound
case ErrCodeUnauthorized:
return http.StatusUnauthorized
case ErrCodeForbidden:
return http.StatusForbidden
case ErrCodeTimeout:
return http.StatusGatewayTimeout
default:
return http.StatusInternalServerError
}
}
func (c ErrorCode) String() string {
names := map[ErrorCode]string{
ErrCodeUnknown: "UNKNOWN",
ErrCodeValidation: "VALIDATION_ERROR",
ErrCodeNotFound: "NOT_FOUND",
ErrCodeUnauthorized: "UNAUTHORIZED",
ErrCodeForbidden: "FORBIDDEN",
ErrCodeTimeout: "TIMEOUT",
ErrCodeDatabase: "DATABASE_ERROR",
ErrCodeNetwork: "NETWORK_ERROR",
ErrCodeInternal: "INTERNAL_ERROR",
}
if name, ok := names[c]; ok {
return name
}
return "UNKNOWN"
}
// 应用错误类型
type AppError struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
Details any `json:"details,omitempty"`
Cause error `json:"-"`
}
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Cause
}
// JSON 输出(用于 API 响应)
func (e *AppError) JSON() []byte {
data, _ := json.Marshal(e)
return data
}
// 错误构造函数
func NewAppError(code ErrorCode, message string) *AppError {
return &AppError{Code: code, Message: message}
}
func NewAppErrorWithCause(code ErrorCode, message string, cause error) *AppError {
return &AppError{Code: code, Message: message, Cause: cause}
}
func ValidationError(message string, details any) *AppError {
return &AppError{
Code: ErrCodeValidation,
Message: message,
Details: details,
}
}
func NotFoundError(resource string) *AppError {
return &AppError{
Code: ErrCodeNotFound,
Message: fmt.Sprintf("%s not found", resource),
}
}
func main() {
// 创建错误
err := ValidationError("invalid request", map[string]string{
"email": "invalid email format",
"age": "must be positive",
})
fmt.Println("错误:", err)
fmt.Println("JSON:", string(err.JSON()))
fmt.Printf("HTTP 状态码: %d\n", err.Code.HTTPStatus())
// 带原因的错误
dbErr := fmt.Errorf("connection refused")
appErr := NewAppErrorWithCause(ErrCodeDatabase, "failed to fetch user", dbErr)
fmt.Println("\n数据库错误:", appErr)
}
4.2 运维场景:操作结果类型
package main
import (
"fmt"
"time"
)
// 操作结果(成功或失败都有详细信息)
type OperationResult struct {
Success bool
Message string
Duration time.Duration
Error error
Retries int
Timestamp time.Time
}
func (r *OperationResult) String() string {
status := "SUCCESS"
if !r.Success {
status = "FAILED"
}
return fmt.Sprintf("[%s] %s (duration: %v, retries: %d)",
status, r.Message, r.Duration, r.Retries)
}
// 批量操作结果
type BatchResult struct {
Total int
Succeeded int
Failed int
Results map[string]*OperationResult
}
func NewBatchResult() *BatchResult {
return &BatchResult{
Results: make(map[string]*OperationResult),
}
}
func (b *BatchResult) Add(key string, result *OperationResult) {
b.Total++
if result.Success {
b.Succeeded++
} else {
b.Failed++
}
b.Results[key] = result
}
func (b *BatchResult) HasErrors() bool {
return b.Failed > 0
}
func (b *BatchResult) Summary() string {
return fmt.Sprintf("Total: %d, Succeeded: %d, Failed: %d",
b.Total, b.Succeeded, b.Failed)
}
// 模拟部署操作
func deployToServer(server string) *OperationResult {
start := time.Now()
result := &OperationResult{
Timestamp: start,
}
// 模拟部署(某些服务器失败)
time.Sleep(10 * time.Millisecond)
if server == "web-02" {
result.Success = false
result.Message = fmt.Sprintf("Deploy to %s failed", server)
result.Error = fmt.Errorf("SSH connection timeout")
result.Retries = 3
} else {
result.Success = true
result.Message = fmt.Sprintf("Deploy to %s completed", server)
}
result.Duration = time.Since(start)
return result
}
func main() {
servers := []string{"web-01", "web-02", "web-03", "web-04"}
batch := NewBatchResult()
fmt.Println("开始部署...")
for _, server := range servers {
result := deployToServer(server)
batch.Add(server, result)
fmt.Println(" ", result)
}
fmt.Println("\n" + batch.Summary())
if batch.HasErrors() {
fmt.Println("\n失败详情:")
for key, result := range batch.Results {
if !result.Success {
fmt.Printf(" %s: %v\n", key, result.Error)
}
}
}
}
五、panic 与 recover
5.1 panic 基础
package main
import "fmt"
func main() {
// panic 会终止程序执行
// 应该仅用于不可恢复的错误
// 常见的 panic 场景:
// 1. 数组越界
// arr := []int{1, 2, 3}
// fmt.Println(arr[10]) // panic: index out of range
// 2. 空指针解引用
// var p *int
// fmt.Println(*p) // panic: nil pointer dereference
// 3. 类型断言失败
// var i interface{} = "hello"
// n := i.(int) // panic: interface conversion
// 4. 关闭已关闭的 channel
// ch := make(chan int)
// close(ch)
// close(ch) // panic: close of closed channel
// 5. 手动 panic
// panic("something terrible happened")
fmt.Println("正常执行")
mayPanic(false)
fmt.Println("继续执行")
// 这里会 panic
// mayPanic(true)
// fmt.Println("不会执行到这里")
}
func mayPanic(shouldPanic bool) {
if shouldPanic {
panic("manual panic")
}
fmt.Println("mayPanic 正常完成")
}
5.2 recover 恢复
package main
import (
"fmt"
"runtime/debug"
)
// recover 必须在 defer 函数中调用
func safeCall(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
// 将 panic 转换为 error
err = fmt.Errorf("panic recovered: %v", r)
// 可选:打印堆栈
fmt.Println("Stack trace:")
debug.PrintStack()
}
}()
fn()
return nil
}
func riskyOperation() {
panic("something went wrong!")
}
func divideByZero() {
var a, b int = 1, 0
fmt.Println(a / b) // panic: integer divide by zero
}
func nilMapAccess() {
var m map[string]int
m["key"] = 1 // panic: assignment to entry in nil map
}
// 运维场景:安全执行任务
func safeExecute(taskName string, task func() error) error {
defer func() {
if r := recover(); r != nil {
fmt.Printf("[%s] PANIC: %v\n", taskName, r)
}
}()
if err := task(); err != nil {
return fmt.Errorf("[%s] error: %w", taskName, err)
}
return nil
}
func main() {
// 恢复 panic
err := safeCall(riskyOperation)
if err != nil {
fmt.Println("捕获错误:", err)
}
fmt.Println("\n程序继续运行...")
// 安全执行多个任务
tasks := map[string]func() error{
"task1": func() error {
fmt.Println("执行 task1")
return nil
},
"task2": func() error {
panic("task2 崩溃了")
},
"task3": func() error {
fmt.Println("执行 task3")
return nil
},
}
for name, task := range tasks {
safeExecute(name, task)
}
fmt.Println("\n所有任务处理完毕")
}
5.3 HTTP 服务器中的 recover
package main
import (
"fmt"
"log"
"net/http"
"runtime/debug"
)
// Recover 中间件
func RecoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
// 记录错误和堆栈
log.Printf("Panic recovered: %v\n%s", err, debug.Stack())
// 返回 500 错误
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal Server Error"))
}
}()
next.ServeHTTP(w, r)
})
}
// 可能 panic 的处理器
func riskyHandler(w http.ResponseWriter, r *http.Request) {
// 模拟 panic
if r.URL.Query().Get("panic") == "true" {
panic("intentional panic for testing")
}
w.Write([]byte("OK"))
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", riskyHandler)
// 使用 recover 中间件包装
handler := RecoverMiddleware(mux)
fmt.Println("服务器启动在 :8080")
fmt.Println("访问 /?panic=true 触发 panic")
// log.Fatal(http.ListenAndServe(":8080", handler))
_ = handler
}
六、错误处理最佳实践
6.1 错误处理模式
package main
import (
"errors"
"fmt"
"io"
"os"
)
// 1. 立即处理模式
func processFileImmediate(path string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("open file: %w", err)
}
defer f.Close()
data, err := io.ReadAll(f)
if err != nil {
return fmt.Errorf("read file: %w", err)
}
fmt.Printf("Read %d bytes\n", len(data))
return nil
}
// 2. 错误延迟处理(defer 中处理)
func processWithDefer(path string) (err error) {
f, err := os.Open(path)
if err != nil {
return err
}
defer func() {
closeErr := f.Close()
if err == nil {
err = closeErr
}
}()
// 处理文件...
return nil
}
// 3. 哨兵错误模式
var (
ErrInvalidInput = errors.New("invalid input")
ErrNotReady = errors.New("service not ready")
)
func doSomething(input string) error {
if input == "" {
return ErrInvalidInput
}
return nil
}
func caller() {
err := doSomething("")
if errors.Is(err, ErrInvalidInput) {
fmt.Println("输入无效,请重试")
}
}
// 4. 错误类型模式
type ConfigError struct {
Key string
Message string
}
func (e *ConfigError) Error() string {
return fmt.Sprintf("config error [%s]: %s", e.Key, e.Message)
}
func loadConfig(key string) (string, error) {
if key == "" {
return "", &ConfigError{Key: key, Message: "key cannot be empty"}
}
return "value", nil
}
// 5. 包装错误添加上下文
func fetchUser(id int) (string, error) {
user, err := queryDB(id)
if err != nil {
return "", fmt.Errorf("fetch user %d: %w", id, err)
}
return user, nil
}
func queryDB(id int) (string, error) {
return "", errors.New("connection refused")
}
func main() {
// 演示各种模式
if err := processFileImmediate("/nonexistent"); err != nil {
fmt.Println("错误:", err)
}
caller()
_, err := fetchUser(123)
if err != nil {
fmt.Println("获取用户失败:", err)
}
}
6.2 运维工具错误处理
package main
import (
"context"
"errors"
"fmt"
"time"
)
// 错误分类
type ErrorCategory int
const (
CategoryTransient ErrorCategory = iota // 临时错误,可重试
CategoryPermanent // 永久错误,无法恢复
CategoryTimeout // 超时错误
)
type OperationError struct {
Category ErrorCategory
Message string
Cause error
Retried int
}
func (e *OperationError) Error() string {
return e.Message
}
func (e *OperationError) Unwrap() error {
return e.Cause
}
func (e *OperationError) IsRetryable() bool {
return e.Category == CategoryTransient
}
// 带重试的操作执行
func executeWithRetry(
ctx context.Context,
name string,
maxRetries int,
operation func() error,
) error {
var lastErr error
for attempt := 0; attempt <= maxRetries; attempt++ {
if attempt > 0 {
// 指数退避
backoff := time.Duration(attempt*attempt) * 100 * time.Millisecond
select {
case <-ctx.Done():
return &OperationError{
Category: CategoryTimeout,
Message: fmt.Sprintf("%s: context cancelled", name),
Cause: ctx.Err(),
Retried: attempt - 1,
}
case <-time.After(backoff):
}
}
err := operation()
if err == nil {
if attempt > 0 {
fmt.Printf("[%s] 成功(重试 %d 次后)\n", name, attempt)
}
return nil
}
lastErr = err
// 检查是否可重试
var opErr *OperationError
if errors.As(err, &opErr) && !opErr.IsRetryable() {
return err // 不可重试的错误,立即返回
}
fmt.Printf("[%s] 尝试 %d 失败: %v\n", name, attempt+1, err)
}
return &OperationError{
Category: CategoryTransient,
Message: fmt.Sprintf("%s: max retries exceeded", name),
Cause: lastErr,
Retried: maxRetries,
}
}
// 模拟不稳定的操作
var callCount int
func unstableOperation() error {
callCount++
if callCount < 3 {
return &OperationError{
Category: CategoryTransient,
Message: "temporary failure",
}
}
return nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := executeWithRetry(ctx, "unstable-op", 5, unstableOperation)
if err != nil {
fmt.Println("最终失败:", err)
} else {
fmt.Println("操作成功")
}
}
七、本章小结
| 主题 | 核心要点 |
|---|---|
| error 接口 | 简单接口,任何实现 Error() 的类型都是错误 |
| 错误检查 | 必须显式处理,使用 if err != nil |
| 错误包装 | 使用 %w 包装,errors.Is/As 检查 |
| 哨兵错误 | 预定义错误值,用于比较 |
| 自定义错误 | 实现 error 接口,添加上下文信息 |
| panic/recover | 仅用于不可恢复的错误,HTTP 中间件恢复 |
| 重试机制 | 区分可重试和不可重试错误 |
运维开发建议
- 总是检查并处理错误,不要使用
_忽略 - 使用错误包装添加上下文:
fmt.Errorf("operation: %w", err) - 区分可重试和不可重试错误,实现智能重试
- 在 HTTP/RPC 服务中使用 recover 中间件
- 记录完整的错误链用于故障排查