谈谈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程序。

### Golang初级开发面试题及答案 以下是针对Golang初级开发者的一些常见面试题目及其解答: #### 1. **Go语言的特点是什么?** Go语言的主要特点包括高效、简洁以及强大的并发支持。它通过goroutine和channel简化了并发编程,使编写高并发应用程序变得容易[^3]。 #### 2. **什么是`defer`关键字?它的作用是什么?** `defer`用于延迟函数调用直到周围函数返回时才执行。这在资源管理方面非常有用,比如文件关闭或者解锁互斥锁等操作。需要注意的是,多个`defer`语句会按照后进先出(LIFO)顺序执行[^2]。 ```go func main() { defer fmt.Println("world") fmt.Println("hello") } // 输出将是: // hello // world ``` #### 3. **如何理解Go中的接口实现机制?** Go采用隐式接口实现方式,只要一个类型实现了某个接口定义的所有方法,则该类型就自动满足这个接口的要求,无需显式的声明[^1]。 #### 4. **解释一下`map`的初始化与使用场景?** `map`是一种键值对集合,在Go中可以通过`make(map[K]V)`来创建。如果需要预分配容量可以指定第二个参数给定大小。当访问不存在的key时,默认返回零值;因此判断是否存在特定key需借助第二返回值形式[^1]。 ```go m := make(map[string]int, 10) value, exists := m["test"] if !exists { fmt.Println("Key does not exist.") } else { fmt.Printf("Value is %d\n", value) } ``` #### 5. **谈谈你对goroutines的理解?** Goroutines是轻量级线程的概念体现,启动成本极低,适合处理大量并行任务。它们由runtime调度器管理和分配到操作系统级别的线程上运行[^3]。 #### 6. **描述下error类型的用途?** Error是一个内置接口,任何自定义错误都应该遵循此标准以便统一处理逻辑。通常我们会结合工厂模式构建具体的错误实例传递回上调用者层面上去决定后续动作。 ```go type MyError struct{} func (e *MyError) Error() string { return "my error message" } err := &MyError{} if err != nil { log.Fatal(err.Error()) } ``` #### 7. **关于slice的操作有哪些注意事项?** Slice是对底层数组的一个视图窗口,修改其中一个会影响另一个共享相同部分的数据副本。另外要注意capacity概念,超出范围追加元素可能引发重新分配内存空间的行为[^2]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值