谈谈golang的错误处理

一、引入

我们在代码编写的过程中,对于各种错误的处理是不可缺少的,而golang由于其并发和简洁的特性,优雅的错误处理显得尤为重要。错误处理不仅能够提高程序的健壮性,还能在出现问题时提供清晰的调试信息。

二、最直观的错误处理

Go开发者通常会使用errors.New()fmt.Errorf()来快速创建错误。这些方法简单直接,适用于不需要复杂错误信息处理的场景。

ini 复制代码
var e error
e = errors.New("基础错误信息")
fmt.Println(e)
 
e = fmt.Errorf("格式化错误信息,%s", "附加详情")
fmt.Println(e)

这种方式有个很明显的弊端:不利于错误比较和匹配。由于错误仅作为字符串存在,使用==进行错误比较时,如果错误消息有任何微小的变化,都会导致比较失败,这限制了错误匹配的灵活性。此外,这种方式不利于代码的可维护性,随着项目规模的扩大,简单的错误处理方式可能导致错误处理代码的膨胀,使得代码难以维护和扩展。如果错误处理有段位的话,以上只能算作青铜。

三、白银

首先提一个概念:哨兵错误。哨兵错误是指在计算机编程中,使用一个特定的值来表示不可能进一步处理的做法,通常在Go语言编程中使用,用于在包内先定义一些错误,然后在包外进行错误值的判断。

golang官方的os标准库中,是这样定义的:

ini 复制代码
package os
...
 
var (
 ErrInvalid = fs.ErrInvalid // "invalid argument"
 ErrPermission = fs.ErrPermission // "permission denied"
 ErrExist      = fs.ErrExist      // "file already exists"
 ErrNotExist   = fs.ErrNotExist   // "file does not exist"
 ErrClosed     = fs.ErrClosed     // "file already closed"

在判断是否文件不存在的时候,我们可以直接通过返回的error来判断:

ini 复制代码
if e == os.ErrNotExist   {
 fmt.Println("not exist")
}

但是官方的用法其实也存在弊端,哨兵错误通常是全局定义的,这可能导致全局命名空间的污染,尤其是在大型项目中,不同模块可能会定义相同名称的错误,造成冲突。而且哨兵错误通常只包含一个简单的错误消息,不包含额外的上下文信息,如错误码、错误发生的具体位置等,这限制了错误的表达能力和调试能力。

四、钻石

go语言中的错误定义是一个接口,只要是声明了 Error() string 这个方法,就意味着它实现了Error接口。

go 复制代码
type error interface {
 Error() string
}

咱们可以像这样自定义它:

csharp 复制代码
type MyError string
 
func (this MyError) Error() string {
  return string(this)
}

为了加入更多自定义属性,可以把string换成struct,以适应更复杂的业务需求。示例如下

go 复制代码
type MyError struct {
  Code int
  Msg  string
}
 
func NewMyError(code int, msg string) *MyError {
  return &MyError{Code: code, Msg: msg}
}
 
func (this MyError) Error() string {
  return fmt.Sprintf("%d-%s", this.Code, this.Msg)
}
 
func DoSomething () error {
  return NewMyError(404, "找不到内容")
}
 
func main() {
  var e error
  e =  DoSomething()
  fmt.Println(e)
}

但是我们在排错的过程中,通常还需要知道出错的更多信息,比如代码的上下文等,只有这样我们才更容易的定位问题。要是想对返回的error附加更多的信息后再返回,我们只能先通过Error方法,取出原来的错误信息,然后自己再拼接,再使用errors.New函数生成新错误返回。这样处理方式还是比较粗放。

五、王者

王者阶段的开发者不仅关注错误的创建和比较,还开始考虑错误完整性。

在大型项目中,通常采用数据层、交互层、Web 服务层分层开发,应该遵循以下原则:

1、一个 error,应该只被处理一次

2、让 error 包含更多的信息

3、原始 error,应保证完整性,不被破坏

4、error 需要被日志记录

我们需要把错误返回到最上层,即将错误收集反馈在 Web 服务层,只在一个地方处理错误。

github.com/pkg/errors库为例,我们可以使用New函数,生成的错误,自带调用堆栈信息。

go 复制代码
func New(message string) error 

如果有一个现成的error,我们需要对他进行再次包装处理,这时候有三个函数可以选择。利用这些函数,在dao层的错误(如gorm操作错误)的外面包装一层,在不影响原始错误的情况下,创建一个堆栈跟踪。

go 复制代码
//只附加新的信息
func WithMessage(err error, message string) error
 
//只附加调用堆栈信息
func WithStack(err error) error
 
//同时附加堆栈和信息
func Wrap(err error, message string) error

在Web服务层或MiddleWare可以通过Cause去handle这些错误信息。

go 复制代码
func Cause(err error) error {
    type causer interface {
        Cause() error
    }
 
    for err != nil {
        cause, ok := err.(causer)
        if !ok {
            break
        }
        err = cause.Cause()
    }
    return err
}

fmt.Errorf 的 %w 动词功能对标 pkg/errors 包中的 errors.Wrap 函数,用法如下:

go 复制代码
package main
 
import (
 "errors"
 "fmt"
)
 
func Foo() error {
 return errors.New("foo error")
}
 
func Bar() error {
 err := Foo()
 if err != nil {
  return fmt.Errorf("bar: %w", err)
 }
 return nil
}
 
func main() {
 err := Bar()
 if err != nil {
  fmt.Printf("err: %s\n", err)
 }
}

errors.Unwrap 函数对标 pkg/errors 包中的 errors.Cause 函数,用法如下:

go 复制代码
func Foo() error {
 return io.EOF
}
 
func Bar() error {
 err := Foo()
 if err != nil {
  return fmt.Errorf("bar: %w", err)
 }
 return nil
}
 
func main() {
 err := Bar()
 if err != nil {
  if errors.Unwrap(err) == io.EOF {
   fmt.Println("EOF err")
   return
  }
  fmt.Printf("err: %+v\n", err)
 }
 return
}

本文基于 MVC 分层结构进行介绍,但实际上大多数项目的分层结构可能各不相同,因此在确定错误处理方式和策略时需要考虑具体情况。

六、总结

Go语言的错误处理机制随着开发者对问题理解的深入而逐步演变。通过合理使用error接口、错误封装、链式错误处理等,开发者可以构建出更加健壮和易于维护的Golang程序。

相关推荐
.生产的驴1 小时前
SpringBoot 对接第三方登录 手机号登录 手机号验证 微信小程序登录 结合Redis SaToken
java·spring boot·redis·后端·缓存·微信小程序·maven
顽疲1 小时前
springboot vue 会员收银系统 含源码 开发流程
vue.js·spring boot·后端
机器之心1 小时前
AAAI 2025|时间序列演进也是种扩散过程?基于移动自回归的时序扩散预测模型
人工智能·后端
hanglove_lucky3 小时前
本地摄像头视频流在html中打开
前端·后端·html
皓木.4 小时前
(自用)配置文件优先级、SpringBoot原理、Maven私服
java·spring boot·后端
i7i8i9com4 小时前
java 1.8+springboot文件上传+vue3+ts+antdv
java·spring boot·后端
秋意钟4 小时前
Spring框架处理时间类型格式
java·后端·spring
我叫啥都行4 小时前
计算机基础复习12.22
java·jvm·redis·后端·mysql
Stark、5 小时前
【Linux】文件IO--fcntl/lseek/阻塞与非阻塞/文件偏移
linux·运维·服务器·c语言·后端
coding侠客5 小时前
Spring Boot 多数据源解决方案:dynamic-datasource-spring-boot-starter 的奥秘
java·spring boot·后端