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