Hi,我是行舟,今天和大家一起学习 Go 语言错误处理包:errors。
Go 语言自身的 errors:https://golang.google.cn/pkg/errors/ 包实现非常简单,使用起来非常灵活,但是又有很多不足。我们在分析 Go 语言 errors 包的同时,也介绍下一个开源的 errors 包:https://pkg.go.dev/github.com/pkg/errors。
Go 语言 errors 包对外暴露了四个方法:
func As(err erros, target any) bool
func Is(err, target error) bool
func New(text string) error
func Unwrap(err error) error
As 方法的作用是在第一个参数 err 的调用链中寻找和第二个参数 target 类型相同的的错误,并把找到的第一个 err 的值赋给 target ,然后返回 true;如果没有找到则返回 false。
import (
"errors"
"fmt"
"io/fs"
"os"
)
type MyError struct {
Op string
Path string
Err error
}
func (e *MyError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }
func main() {
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
var myError *MyError
if errors.As(err, &pathError) {
fmt.Println("PathError:", pathError.Path) // PathError: non-existing
}
if errors.As(err, &myError) { // 在 err 的调用链中,没有找到 MyError 类型
fmt.Println("MyError:", pathError.Path) // 没有打印任何结果
}
fmt.Println(err)
}
}
相比较于 As 方法,Is 只判断第一个参数 err 的调用链中是否存在和第二个参数 target 类型相同的的错误,存在则返回 true,否则返回 false。
New 方法返回 error 类型,同时指定文本内容为传入的 text 值。
import (
"errors"
"fmt"
)
func main() {
err1 := errors.New("I am an error.")
err2 := errors.New("I am an error.")
fmt.Println(err1 == err2) // false
}
上例中,我们定义了字符串相同的两个 error,在比较时,他们是不相等的。
看一下 error 包中 New 方法的实现:
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
可以看到 New 方法每次返回的是指针地址,所以每次调用返回的都是不同的地址。
这个方法对于初次接触 Go 语言的同学其实有点懵。要理解为什么有 Unwrap 方法,首先需要知道 Wrap errors。当我们在代码中想要对原有错误进行扩展时,通常可以使用两种方式:
newError := fmt.Errof("some reasons: %v", err)
方法对 err 进行包装,返回新的 error。type MyError struct {
Op string
Path string
Err error
}
其中 fmt.Errof()
方法,在使用 %w 时,返回了一个 Wrapping error。
import (
"errors"
"fmt"
)
func main() {
err1 := errors.New("I am err1.")
err2 := fmt.Errorf("I am err2 , %w", err1) // 通过 %w 可以生成一个 Wrapping error
fmt.Println(errors.Unwrap(err2)) // I am err1.
}
Go 语言本身提供了 Unwrap 方法,并没有提供直接的 Wrap 方法,感觉还是挺奇怪的。
不过有一个 error 的开源仓库实现了,error的一些扩展方法。
仓库地址:https://pkg.go.dev/github.com/pkg/errors
它实现了更加丰富的 error 方法。
主要介绍下 Wrap 方法,Wrap 方法用来包装原来的 err 并添加扩展信息。
func ReadFile(path string)([]byte, error){
f, err := os.Open(path)
if err != nil{
return nil, errors.Wrap(err, "open failed!") // 通过 Wrap 包装 err,既可以把保留原始错误,又可以添加其他信息
}
defer f.Close()
}
func main(){
_, err := ReadConfig()
if err != nil {
fmt.Printf("original error: %T %v \n", errors.Cause(err), errors.Cause(err)) // 打印报错的地方 Cause 方法的返回值,还可以判断根因类型
fmt.Printf("stack trace: \n %+v \n", err) // 打印错误堆栈
os.Exit(1)
}
}
func ReadConfig()([]byte, error){
home := os.Getenv("home")
config, err := ReafFile(......)
return config, errors.WithMessage(err, "could not read config") // Wrap 会保存堆栈信息,WithMessage不会保存堆栈信息。
}
Wrap errors 是 Go 语言中处理错误很优雅的方法,但是最好是指在开发具体应用代码时使用,基础包不适合使用,因为如果你在基础包里调用了 Wrap ,业务方再调用 Wrap 会导致保留了两次堆栈信息。
为什么需要 Wrap errors ?因为当我们调用基础包返回 error 时,为了方便问题的定位和追踪,往往需要再添加一些提示信息,如果修改原始 error 的提示信息,会导致原始错误提示信息内容被覆盖。Wrap errors 可以既保留原始的 error 链,又能扩展错误提示。
根据 error 的使用形式,我们将其分为三类:
包级别的错误,比如 io 包中的定义。
var EOF = errors.New("EOF")
在使用时,我们只能像下面这样判断错误的类型。
if err == io.EOF {
//...
}
if errors.Is(err, io.EOF){
//...
}
这种定义错误的方式,需要把 error 当作 API 的一部分暴露出来,不是很优雅。
自定义错误类型,在使用时通过类型断言,获取更多的上下文信息。
type MyErrot struct{
Msg string
File string
Line int
}
func (e *MyError) Error() string{
return fmt.Sprintf("%s:%d: %s", e.File, e.Line, e.Msg)
}
func test() error {
return &MyError{"Something happened", "server.go", 42}
}
func main(){
err := test()
switch err := err.(type){
case nil:
// do something success!
case *MyError:
fmt.Println("error occurred on line:", err.Line)
default:
// do something else.
}
}
缺点:通过类型断言和类型 switch,需要让自定义的 error 变成 public。这种模型会导致与调用者产生强耦合,从而导致 API 变得脆弱。
虽然比 Sentinel Error 好一些,但是还是不建议使用,或者说在使用时要避免成为公共 API 的一部分。
灵活的错误处理方式,要求代码和调用者之间的耦合最少。
虽然你知道发生了错误,但是你没有能力看到错误的内部。作为调用者,只关心操作结果就好了。
e.g
func fn() error {
x,err := bar.Foo()
if err != nil { // 不用关心错误细节
return err
}
}
有时候二分错误无法满足,需要更多判断,这种情况下我们可以断言错误实现了特定的行为,而不是断言错误是特定的类型或值。
// 封装方
type Error interface{
error
Timeout() bool // 是否超时
Temporary() bool // 是否临时
}
type temporary interface {
Temporary() bool
}
func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}
// 调用方
func main(){
if nerr, ok := err.(Error) && nerr.Temporary() {
time.Sleep(100)
continue
}
if err != nil {
log.Fatal(err)
}
}
这是非常推荐的一种错误处理方式。
https://golang.google.cn/pkg/errors/
https://pkg.go.dev/github.com/pkg/errors
https://blog.csdn.net/puss0/article/details/116489846
https://u.geekbang.org/lesson/324?article=481322
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有