“ Errors are values ”源⾃于Go语⾔创始⼈之⼀的Rob Pike对error的设计理念。Rob Pike认 为,error和函数的其它返回值地位相等,只是多个返回值的其中之⼀,并⽆任何特殊之处。 因此,对error的处理就如同正常对待⼀个函数的返回值⼀样进⾏。
返回error
errors.New("出现错误")
error获取字符串
errors.error()
返回error(错误拼接)
# 错误拼接
fmt.Errorf("not found db config: %s", "出现错误")
Errorf函数源码:
// a参数是一个interface,可以接收任意类型
func Errorf(format string, a ...interface{}) error {
p := newPrinter()
p.wrapErrs = true
p.doPrintf(format, a)
s := string(p.buf)
var err error
if p.wrappedErr == nil {
err = errors.New(s)
} else {
err = &wrapError{s, p.wrappedErr}
}
p.free()
return err
}
error多个情况
multierror.Append()
github.com/pkg/errors包
将error的需求分为2类:
- ⾯向机器:⾸先需要对这个error进⾏返回,外层根据error直接返回,或者外层需要针对不 同的error进⾏不同的业务逻辑处理。
- ⾯向开发⼈员:在排查问题时,我们需要针对⼀些错误的情况进⾏打印,甚⾄堆栈。在排 查问题时,如果⼀个⽅法在外层被好⼏个⽅法调⽤了,如何区分它是⾛的哪⼀条路径?我 们是不是会⼿动加很多⽇志,甚⾄⾃⼰输出堆栈
func Unwrap(err error) error{
u, ok := err.(interface {
Unwrap() error
})
...
}
func Is(err, target error) bool {
if target == nil {
return err == target
}
...
}
func As(err error, target interface{}) bool {
if target == nil {
panic("errors: target cannot be nil")
}
...
}
New
errors.New: 创建⼀个err,需要指定⼀个错误message,例如:
err := errors.New("it is a error")
fmt.Printf("err 错误为:%v\n", err)
或者 fmt.Errorf:
// 字符串中未包含错误信息
err1 := fmt.Errorf("error1:e1")
fmt.Printf("err1: %v\n", err1)
// 字符串中包含了%w
err2 := fmt.Errorf("error2:%w", err1)
fmt.Printf("err2: %v\n", err2)
使⽤%w创建的包装错误可⽤于errors.Is和errors.As:
Is
errors.Is(err error, target error) bool
使⽤场景:希望判断err⾥⾯是否包含某个错,因为error可能会被包装很多层,⽐如经常我们 的数据库报错了,那么上⼀层会再进⾏包装,⼀直到最上⼀层的话,可能err⾥⾯包含了很多 信息
if errors.Is(err, redis.Nil) {//不论error被包了多少层,都能这么⽤
// 缓存失效,读取数据库,载⼊redis
return err
}
## As errors.As(err error, target interface{}) bool ⽅法会解包所有err⾥包装的error,并且看是否能类型转换为target的类型,如果可以,则将转换后的结果赋值到target,并返回true,否则false。target必须是指针
使⽤场景:当我们只关⼼是否是某⼀类错误,例如有redis,mysql错误,但是只关⼼是否 是redis错误,此时就可以⽤这个⽅法了。
if _, err := os.Open("non-existing"); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("Failed at path:", pathError.Path)
} else {
fmt.Println(err)
}
}
os.Open会返回有2种⾃定义error,⽐如syscall.Errno,os.PathError,当你 只关⼼os.PathError错误类型,就可以⽤As⽅法了。
Wrap
Wrap ⽅法⽤来包装底层错误,增加上下⽂⽂本信息并附加调⽤栈。 ⼀般⽤于包装对第三⽅代 码(标准库或第三⽅库)的调⽤。当输出时fmt.Printf或者fmt.Sprinitf使⽤%+v则会输出堆栈 信息(具体原理可⻅源码)
func TestWrap(t *testing.T) {
d := errors2.Wrap(sql.ErrNoRows, fmt.Sprintf("no user with id %d\n", 123))
fmt.Printf("%+v\n", d)
}
返回
sql: no rows in result set
no user with id 123
main.foo
/Users/qimao/goProject/demo00/error.go:10
main.main
/Users/qimao/goProject/demo00/error.go:13
runtime.main
/usr/local/go/src/runtime/proc.go:225
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1371
WithMessage
WithMessage ⽅法仅增加上下⽂⽂本信息,不附加调⽤栈。 如果确定错误已被 Wrap 过或不 关⼼调⽤栈,可以使⽤此⽅法。 注意:不要反复 Wrap ,会导致调⽤栈重复
func TestWithMessage(t *testing.T) {
d := errors2.WithMessage(sql.ErrNoRows, fmt.Sprintf("no user with id %d\n", 123))
fmt.Printf("%+v\n", d)
}
结果
sql: no rows in result set
no user with id 123
Cause
Cause⽅法⽤来判断底层错误 。让我们可以获得最根本的错误原因
WithStack
如果不需要增加额外上下⽂信息,仅附加调⽤栈后返回,可以使⽤ WithStack ⽅法。它和 Wrap唯⼀的区别就是不能传⼊message。硬要说具体的使⽤场景,那就是针对官⽅error包返 回的error进⾏类型⽤errors.WithStack使其成为⼀个带堆栈的error
⼯程实践建议
关于error的⼯程实践建议
- error应该是函数的最后⼀个返回值,当 error !=nil 时,函数的其他返回值是不可⽤的状态,不应该对其他返回值做任何期待
- 处理错误时应该时第⼀时间判断错误,当if err != nil出现错误及时返回,使代码是⼀条流畅的直线,避免过多的嵌套.
- 如果是调⽤应⽤程序的其他函数出现错误,请直接返回,如果需要携带信息,请使⽤ errors.WithMessage
- 如果是调⽤其他库(标准库、企业公共库、开源第三⽅库等)获取到错误时,请使⽤ errors.Wrap 添加堆栈信息,不要每个地⽅都是⽤ errors.Wrap 只需要在错误第⼀次出现 时进⾏ errors.Wrap 即可
- 禁⽌每个出错的地⽅都打⽇志,只需要在进程的最开始的地⽅使⽤ %+v 进⾏统⼀打印或
者不返回err时,例如 http/rpc 服务的中间件
err := handle() if err != nil { log.Printf("%+v\n",err) return } //其他处理
- 错误判断使⽤ errors.Is 进⾏⽐较
- 对于业务错误,推荐在⼀个统⼀的地⽅创建⼀个错误字典,错误字典⾥⾯应该包含错误的 code,并且在⽇志中作为独⽴字段打印,⽅便做业务告警的判断,错误必须有清晰的错 误⽂档
- 在业务中可以定⼀些包级别或者项⽬级别的,可以使⽤Sentinel Error(哨兵错误),然后
- 在调⽤的时候外部包可以直接对⽐变量进⾏判定,在标准库当中⼤量的使⽤了这种⽅式。 但这样暴露出去也有可能被⼈修改了
- ⼀些优雅的处理错误⽅法,可以参考Go官⽅博客Errors are values
errgroup 并发执行
https://linuxhint.com/golang-errgroup/
Function
- WithContext 返回一个带上下文的new group;
- GO 将指定函数调用到新的 goroutine;
- Wait 等待,知道GO执行结束;
errgroup有个缺陷,go程没有检查rescover,存在panic没有捕获风险!替换包:github.com/go-kratos/kratos/pkg/sync/errgroup (bilibili kratos)
log包
log模块主要提供了3类接口。分别是 “Print 、Panic 、Fatal ”,对每一类接口其提供了3中调用方式,分别是 “Xxxx 、 Xxxxln 、Xxxxf”,基本和fmt中的相关函数类似
package main
import (
"log"
)
func main(){
arr := []int {2,3}
log.Print("Print array ",arr,"\n")
log.Println("Println array",arr)
log.Printf("Printf array with item [%d,%d]\n",arr[0],arr[1])
}
程序输出结果:
2016/12/15 19:46:19 Print array [2 3]
2016/12/15 19:46:19 Println array [2 3]
2016/12/15 19:46:19 Printf array with item [2,3]
-
对于 log.Fatal 接口,会先将日志内容打印到标准输出,接着调用系统的 os.exit(1) 接口,退出程序并返回状态 1 。但是有一点需要注意,由于是直接调用系统接口退出,defer函数不会被调用
-
对于log.Panic接口,该函数把日志内容刷到标准错误后调用 panic 函数;
-
也可以自定义Logger类型, log.Logger提供了一个New方法用来创建对象:
func New(out io.Writer, prefix string, flag int) *Logger
该函数一共有三个参数:
(1)输出位置out,是一个io.Writer对象,该对象可以是一个文件也可以是实现了该接口的对象。通常我们可以用这个来指定日志输出到哪个文件。 (2)prefix 我们在前面已经看到,就是在日志内容前面的东西。我们可以将其置为 “[Info]” 、 “[Warning]”等来帮助区分日志级别。 (3) flags 是一个选项,显示日志开头的东西,可选的值有
Ldate = 1 << iota // 形如 2009/01/23 的日期
Ltime // 形如 01:23:23 的时间
Lmicroseconds // 形如 01:23:23.123123 的时间
Llongfile // 全路径文件名和行号: /a/b/c/d.go:23
Lshortfile // 文件名和行号: d.go:23
LstdFlags = Ldate | Ltime // 日期和时间
package main
import (
"log"
"os"
)
func main(){
fileName := "Info_First.log"
logFile,err := os.Create(fileName)
defer logFile.Close()
if err != nil {
log.Fatalln("open file error")
}
debugLog := log.New(logFile,"[Info]",log.Llongfile)
debugLog.Println("A Info message here")
debugLog.SetPrefix("[Debug]")
debugLog.Println("A Debug Message here ")
}
生成文件:Info_First.log
[Info]F:/works/Golang/src/awesomeProject/main/main.go:57: A Info message here
[Debug]F:/works/Golang/src/awesomeProject/main/main.go:59: A Debug Message here
Refer
https://github.com/pkg/errors