Go error

2020/01/12 Go 知识点

“ 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

Search

    Table of Contents