errors是Go语言官方提供的错误包,里面代码异常简洁,这里我贴一下:
// because the former will succeed if err wraps an *fs.PathError.
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
}
func (e *errorString) Error() string {
return e.s
}
type error interface {
Error() string
}
errors包里面包含一个对外的方法New,主要用于自定义错误信息。例如:
func test(a, b int) {
if a > b {
errors.New("a比b大,抛出异常")
}
}
这里New方法实际上是返回的是一个实现了error接口的类型的变量,这里是errorString结构体变量。error接口是Go语言官方提供的接口,里面含有一个方法Error() string,而errorString结构体实现了这个接口。
package main
import (
"errors"
"fmt"
)
func main() {
err := test(3, 1)
if err != nil {
fmt.Println(err)
}
}
func test(a, b int) error {
if a > b {
return errors.New("a比b大,抛出异常")
}
return nil
}
test函数遇到逻辑错误,构建error类型的结构体变量返回;main函数判断err不等于nil,打印错误。
哨兵错误。程序中遇到一个事先约定好的错误,处理流程不能再进行下去了,必须停下来,例如:
func readFile(path string) error {
err := openFile(path)
if err == io.EOF {
log.Fatal("read failed:", err)
}
}
第三行err == io.EOF是说error等于事先约定好的 io.EOF这个错误就程序停止往下执行。这里像一个问题:如果开发人员修改了这个错误或者加一些上下文信息,那上层判断“==”不就失效了吗,这里改如何处理呢?
package main
import (
"fmt"
"io/ioutil"
"os"
)
func main() {
_, err := readFile("errortest/aa.txt")
fmt.Println(err)
}
func readFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open failed: %w", err)
}
defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("read failed: %w", err)
}
return buf, nil
}
func Unwrap(err error) error {
u, ok := err.(interface{ Unwrap() error })
if !ok {
return nil
}
return u.Unwrap()
}
上面这个代码readFile读文件,使用了fmt.Errorf(“open failed: %w”, err),在系统错误err本来是“open aa.txt: no such file or directory”,使用“%w”上加了“open failed:”,这样自定义的上下文描述就嵌套在系统错误上面了,返回给上层就是“open failed: open aa.txt: no such file or directory”,如果上层使用类似“err == io.EOF”就有问题了,这里需要先使用Unwarp来把错误自定义的上下文错误剥离出来,再使用类似“err == io.EOF”的语句做逻辑判断。
当然pkg/errors也提供了类似的解决方案。通过Warp可以把一个错误加上一个字符串包装成一个新的错误;通过Cause则可以进行相反的操作,将里层的错误还原。fmt.Printf(“%+v”, err)可以打印出错误堆栈。
package main
import (
"fmt"
"go-gin-api/pkg/errors"
"io/ioutil"
"os"
)
func main() {
_, err := readFile("aa.txt")
//fmt.Println(err)
if err != nil {
fmt.Printf("%+v", err)
os.Exit(1)
}
}
func readFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
//return nil, fmt.Errorf("open failed: %w", err)
return nil, errors.Wrap(err, "open failed")
}
defer f.Close()
buf, err := ioutil.ReadAll(f)
if err != nil {
//return nil, fmt.Errorf("read failed: %w", err)
return nil, errors.Wrap(err, "read failed: %w")
}
return buf, nil
}
error的类型。指的是实现了error接口的哪些类型。他的一个好处是类型中除了error外,还可以附带其它字段,从而提供额外信息。
type PathError struct {
Op string
Path string
Err error
}
题外话:PathError结构体有error字段,说明这个结构体的Err属性是实现了error接口的类型的对象,于是就会有error接口的方法,所有PathError的对象也会有error接口的方法,也就是说PathError实现了error接口。
func getError(err error)error {
switch err := err.(type) {
case *PathError:
return err.Err
case *OtherError:
return err.Err
}
}
这样做的不好的地方是自定义的错误和使用错误的包之间形成了依赖关系,代码不够优雅。
黑盒errors。能知道错误发生了,但是无法看到他内部到底是什么,不知道具体类型是什么。
func fn() error {
x,err := =bar.Foo()
if err !=nil {
return err
}
return nil
}
一旦出错,直接返回,终止后续操作。但是项目开发中比如请求外部接口超时,需要重新请求,这个时候就无法做到。
如上Sentinel errors的处理方式便是检查并优雅鹅处理错误的案例。
if err != nil {
//return nil, fmt.Errorf("open failed: %w", err)
log.Fatal(err)
return nil, errors.Wrap(err, "open failed")
}
这里既吧错误记录到日志了还向上返回了错误。这样做的后果是日志上有好多统一的错误信息。合理做法只处理错误一次,要么记录日志要么向上返回。通常项目开发中底层方法向上返回错误,上层方法记录日志。