一、panic不是世界末日
刚接触Golang时,看到panic这个词总觉得特别吓人——就像程序突然心脏病发作一样。但事实上,它只是Go语言处理不可恢复错误的机制。举个例子:
func main() {
// 模拟一个致命错误
connectDB("invalid_connection_string")
}
func connectDB(connStr string) {
if connStr == "" {
panic("数据库连接字符串不能为空") // 触发panic
}
// 假装连接数据库...
}
当这段代码运行时,控制台会打印堆栈信息然后退出。虽然看起来粗暴,但这正是panic的设计初衷——处理那些"程序已经无法正常继续"的情况。
不过在实际项目中,我们更希望程序能优雅地处理错误而不是直接崩溃。这时候就需要recover来拯救世界了。
二、recover的正确打开方式
recover就像是程序的急救包,必须配合defer使用才有效。看这个升级版的数据库连接示例:
func main() {
safeConnect := func() {
if err := recover(); err != nil {
fmt.Printf("捕获到panic: %v\n", err)
// 这里可以添加告警通知等逻辑
}
}
defer safeConnect()
connectDB("") // 传递空字符串触发panic
fmt.Println("这行不会被执行")
}
func connectDB(connStr string) {
if connStr == "" {
panic("数据库连接字符串不能为空")
}
fmt.Println("数据库连接成功")
}
现在程序不会直接崩溃,而是会打印错误信息后继续运行。注意几个关键点:
- recover必须放在defer函数中
- recover只在直接发生panic的函数中无效
- 恢复后程序会从panic点之后继续执行
三、错误处理的层次艺术
在实际项目中,我们通常采用分层错误处理策略。来看一个Web服务的典型例子:
// 数据访问层
func GetUser(id int) (User, error) {
if id <= 0 {
return User{}, errors.New("无效的用户ID")
}
// 模拟数据库查询
return User{ID: id, Name: "张三"}, nil
}
// 业务逻辑层
func ProcessUserOrder(userID int) (Order, error) {
user, err := GetUser(userID)
if err != nil {
return Order{}, fmt.Errorf("查询用户失败: %w", err) // 错误包装
}
if user.Balance <= 0 {
return Order{}, errors.New("用户余额不足")
}
// 处理订单逻辑...
return Order{ID: "123", Amount: 100}, nil
}
// HTTP接口层
func handleOrderRequest(w http.ResponseWriter, r *http.Request) {
userID, _ := strconv.Atoi(r.URL.Query().Get("user_id"))
order, err := ProcessUserOrder(userID)
if err != nil {
// 根据错误类型返回不同HTTP状态码
if errors.Is(err, ErrUserNotFound) {
w.WriteHeader(http.StatusNotFound)
} else {
w.WriteHeader(http.StatusBadRequest)
}
fmt.Fprintf(w, `{"error": "%s"}`, err.Error())
return
}
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(order)
}
这种分层处理的好处是:
- 每层只处理自己关心的错误
- 通过错误包装保留完整调用链
- 顶层可以统一格式化错误输出
四、实战中的进阶技巧
4.1 自定义错误类型
当需要携带更多错误信息时,可以定义自己的错误类型:
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
Details string `json:"details,omitempty"`
}
func (e APIError) Error() string {
return fmt.Sprintf("%d: %s", e.Code, e.Message)
}
func NewAPIError(code int, msg string) *APIError {
return &APIError{
Code: code,
Message: msg,
}
}
// 使用示例
func GetProduct(id int) (*Product, error) {
if id == 0 {
return nil, NewAPIError(400, "产品ID不能为空")
}
// ...
}
4.2 错误包装与解包
Go 1.13引入了错误包装机制,可以保留错误链:
var ErrDBConnection = errors.New("数据库连接失败")
func ConnectDB() error {
// 模拟连接失败
return fmt.Errorf("%w: 连接超时", ErrDBConnection)
}
func main() {
err := ConnectDB()
if errors.Is(err, ErrDBConnection) { // 错误匹配
fmt.Println("捕获到数据库连接错误:", err)
}
}
4.3 panic与recover的边界
虽然recover很强大,但也要注意使用边界:
- 不要用recover处理业务逻辑错误
- 协程中的panic需要单独recover
- 某些致命错误(如内存不足)无法被recover
看个协程安全的例子:
func safeGo(fn func()) {
defer func() {
if err := recover(); err != nil {
log.Printf("协程panic: %v", err)
}
}()
fn()
}
func main() {
safeGo(func() {
panic("测试协程panic")
})
time.Sleep(time.Second)
fmt.Println("主协程继续执行")
}
五、错误处理哲学
经过这些实践,我总结出几个Go错误处理的黄金法则:
- 能用error解决的问题绝不用panic
- 错误信息要包含足够上下文
- 在程序边界处处理或转换错误
- 文档要明确说明函数可能返回的错误
- 保持错误处理代码的简洁性
记住,好的错误处理不是让程序永远不报错,而是让错误发生时能提供清晰的问题诊断路径。就像老司机开车,不是永远不出事故,而是知道怎么安全处理各种突发状况。
评论