一、引入
我们在代码编写的过程中,对于各种错误的处理是不可缺少的,而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程序。