name: go-error-handling user-invocable: false description: 用于Go错误处理,包括错误包装、哨兵错误和自定义错误类型。在Go应用程序中处理错误时使用。 allowed-tools:
- Bash
- Read
Go 错误处理
掌握Go的错误处理模式,包括错误包装、哨兵错误、自定义错误类型和errors包,用于构建健壮的应用程序。
基本错误处理
创建和返回错误:
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除以零")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("错误:", err)
return
}
fmt.Println("结果:", result)
}
使用 fmt.Errorf:
func processFile(filename string) error {
if filename == "" {
return fmt.Errorf("文件名不能为空")
}
// 处理文件...
return nil
}
错误包装
使用上下文包装错误(Go 1.13+):
import (
"errors"
"fmt"
"os"
)
func readConfig(path string) error {
_, err := os.Open(path)
if err != nil {
return fmt.Errorf("读取配置失败:%w", err)
}
return nil
}
func main() {
err := readConfig("config.json")
if err != nil {
fmt.Println(err)
// 输出:读取配置失败:打开 config.json:没有这样的文件
}
}
解包错误:
func handleError(err error) {
// 解包一层
unwrapped := errors.Unwrap(err)
if unwrapped != nil {
fmt.Println("解包后:", unwrapped)
}
// 检查链中是否有特定错误
if errors.Is(err, os.ErrNotExist) {
fmt.Println("文件不存在")
}
}
哨兵错误
定义和使用哨兵错误:
package main
import (
"errors"
"fmt"
)
var (
ErrNotFound = errors.New("资源未找到")
ErrUnauthorized = errors.New("未经授权的访问")
ErrInvalidInput = errors.New("无效输入")
)
func getUser(id int) (string, error) {
if id < 0 {
return "", ErrInvalidInput
}
if id == 0 {
return "", ErrNotFound
}
return fmt.Sprintf("用户-%d", id), nil
}
func main() {
_, err := getUser(0)
if errors.Is(err, ErrNotFound) {
fmt.Println("用户未找到")
}
}
自定义错误类型
实现错误接口:
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s上的验证错误:%s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "年龄",
Message: "必须为正数",
}
}
if age > 150 {
return &ValidationError{
Field: "年龄",
Message: "必须小于150",
}
}
return nil
}
func main() {
err := validateAge(-5)
if err != nil {
fmt.Println(err)
}
}
使用 errors.As 进行类型断言:
func handleValidation(err error) {
var validationErr *ValidationError
if errors.As(err, &validationErr) {
fmt.Printf("字段 '%s' 失败:%s
",
validationErr.Field,
validationErr.Message,
)
}
}
多错误处理
收集多个错误:
type MultiError struct {
Errors []error
}
func (m *MultiError) Error() string {
if len(m.Errors) == 0 {
return "没有错误"
}
if len(m.Errors) == 1 {
return m.Errors[0].Error()
}
return fmt.Sprintf("发生%d个错误:%v", len(m.Errors), m.Errors)
}
func (m *MultiError) Add(err error) {
if err != nil {
m.Errors = append(m.Errors, err)
}
}
func validateUser(name, email string, age int) error {
errs := &MultiError{}
if name == "" {
errs.Add(errors.New("姓名是必需的"))
}
if email == "" {
errs.Add(errors.New("电子邮件是必需的"))
}
if age < 0 {
errs.Add(errors.New("年龄必须为正数"))
}
if len(errs.Errors) > 0 {
return errs
}
return nil
}
恐慌和恢复
何时使用恐慌:
// 恐慌用于不可恢复的错误
func mustConnect(dsn string) *DB {
db, err := connect(dsn)
if err != nil {
panic(fmt.Sprintf("连接到数据库失败:%v", err))
}
return db
}
// 从恐慌中恢复
func safeExecute(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("恐慌恢复:%v", r)
}
}()
fn()
return nil
}
错误处理模式
早期返回模式:
func processRequest(id int) error {
user, err := fetchUser(id)
if err != nil {
return fmt.Errorf("获取用户:%w", err)
}
if err := validateUser(user); err != nil {
return fmt.Errorf("验证用户:%w", err)
}
if err := saveUser(user); err != nil {
return fmt.Errorf("保存用户:%w", err)
}
return nil
}
错误变量命名:
// 好:具体的错误名称
errDB := connectDB()
if errDB != nil {
return fmt.Errorf("数据库连接:%w", errDB)
}
errCache := connectCache()
if errCache != nil {
return fmt.Errorf("缓存连接:%w", errCache)
}
// 避免:到处重用'err'会使调试更困难
pkg/errors 模式(传统)
import (
"github.com/pkg/errors"
)
func loadConfig() error {
_, err := os.Open("config.json")
if err != nil {
return errors.Wrap(err, "加载配置失败")
}
return nil
}
func init() {
if err := loadConfig(); err != nil {
// 打印堆栈跟踪
fmt.Printf("%+v
", err)
}
}
错误日志记录
结构化错误日志记录:
import (
"log/slog"
)
func processOrder(orderID string) error {
order, err := fetchOrder(orderID)
if err != nil {
slog.Error("获取订单失败",
"orderID", orderID,
"error", err,
)
return fmt.Errorf("获取订单 %s:%w", orderID, err)
}
if err := validateOrder(order); err != nil {
slog.Warn("订单验证失败",
"orderID", orderID,
"error", err,
)
return fmt.Errorf("验证订单:%w", err)
}
return nil
}
HTTP 错误处理
处理 HTTP 错误:
import (
"encoding/json"
"net/http"
)
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func (e *APIError) Error() string {
return e.Message
}
func writeError(w http.ResponseWriter, err error) {
var apiErr *APIError
if errors.As(err, &apiErr) {
w.WriteHeader(apiErr.Code)
json.NewEncoder(w).Encode(apiErr)
return
}
// 默认错误
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(APIError{
Code: http.StatusInternalServerError,
Message: "内部服务器错误",
})
}
func handler(w http.ResponseWriter, r *http.Request) {
err := processRequest(r)
if err != nil {
writeError(w, err)
return
}
w.WriteHeader(http.StatusOK)
}
错误上下文
向错误添加上下文:
type ContextError struct {
Op string // 操作
Path string // 文件路径、URL等
Err error // 底层错误
}
func (e *ContextError) Error() string {
return fmt.Sprintf("%s %s:%v", e.Op, e.Path, e.Err)
}
func (e *ContextError) Unwrap() error {
return e.Err
}
func readFile(path string) error {
_, err := os.ReadFile(path)
if err != nil {
return &ContextError{
Op: "读取",
Path: path,
Err: err,
}
}
return nil
}
测试错误情况
测试错误条件:
package main
import (
"errors"
"testing"
)
func TestDivideByZero(t *testing.T) {
_, err := divide(10, 0)
if err == nil {
t.Fatal("预期错误,得到nil")
}
expected := "除以零"
if err.Error() != expected {
t.Errorf("预期 %q,得到 %q", expected, err.Error())
}
}
func TestErrorWrapping(t *testing.T) {
err := readConfig("missing.json")
if err == nil {
t.Fatal("预期错误")
}
if !errors.Is(err, os.ErrNotExist) {
t.Error("预期包装的 ErrNotExist")
}
}
func TestCustomError(t *testing.T) {
err := validateAge(-1)
var validationErr *ValidationError
if !errors.As(err, &validationErr) {
t.Fatal("预期 ValidationError")
}
if validationErr.Field != "年龄" {
t.Errorf("预期字段 '年龄',得到 %q", validationErr.Field)
}
}
何时使用此技能
使用 go-error-handling 当您需要:
- 正确处理Go应用程序中的错误
- 添加错误上下文而不丢失信息
- 定义领域特定的错误类型
- 检查特定错误条件
- 使用额外上下文包装错误
- 以适当细节记录错误
- 从HTTP处理器返回错误
- 彻底测试错误条件
- 构建错误恢复系统
- 基于错误类型实现重试逻辑
最佳实践
- 始终检查错误,从不忽略它们
- 返回错误而不是记录并继续
- 使用 fmt.Errorf 和 %w 包装错误
- 使用 errors.Is 比较哨兵错误
- 使用 errors.As 进行类型断言
- 在错误消息中提供上下文
- 使用自定义错误类型处理领域错误
- 不在库中使用恐慌,返回错误
- 以适当级别记录错误
- 像测试成功路径一样彻底测试错误路径
常见陷阱
- 使用 _ 赋值忽略错误
- 不包装错误(丢失上下文)
- 使用 == 进行错误比较
- 恐慌而不是返回错误
- 不处理所有错误情况
- 创建太多自定义错误类型
- 错误消息格式不佳
- 不测试错误条件
- 在goroutine中吞没错误
- 错误中没有提供足够上下文