1. panic

在什么情况下使用panic?

  1. 在程序启动的时候,如果有强依赖的服务出现故障时panic退出
  2. 在程序启动的时候,如果发现有配置明显不符合要求,可以panic退出(预防编程)
  3. 其他情况下只要不是不可恢复的程序错误,都不应该直接panic,应该返回error
  4. 在程序入口处,例如gin中间件需要使用recover 预防panic程序退出
  5. 在程序中应该避免使用野生的goroutine
    • 如果是在请求中需要执行异步任务,应该使用异步worker消息通知的方式进行处理,避免请求量大时大量的goroutine创建
    • 如果入要使用goroutine时,应该使用统一的Go函数创建,这个函数中会进行recover,避免因为野生goroutine panic 导致主程序退出
    func Go(f func()){
    go func(){
    // defer recover 捕获panic
    defer func(){
    if err := recover(); err != nil {
    log.Printf("panic: %+v", err)
    }
    }() f()
    }()
    }

2.error

在什么情况下使用error?

  1. 在应用程序中使用github.com/pkg/errors 处理相应的错误, 注意在公共库中,一般不使用这个

  2. error 应该是函数的最后一个返回值,当 error 不为 nil 时,函数的其他返回值是不可用的状态,不应该对其他返回值做任何期待

    • 比如func f()(io.Reader, *S1,error)这里,如果error 不为nil,我们不知道io.Reader中是否有返回值,可能有,也可能没有,也有可能只有一部分
  3. 错误处理的时候,应该先判断错误,if err != nil 出现错误时及时返回,使代码是一条流畅的直线, 避免过多的嵌套

    // good case
    func f() error {
    a, err := A()
    if err != nil {
    return err
    } // ... 其他逻辑
    return nil
    }
    // bad case
    func f() error {
    a, err := A()
    if err == nil {
    // 其他逻辑
    } return err
    }
  4. 在 应用程序中出现错误时,使用errors.New或者errors,Errorf返回错误

    func (u *usecese) usecase1() error {
    money := u.repo.getMoney(uid)
    if money < 10 {
    return errors.Errorf("用户余额不足, uid: %d, money: %d", uid, money)
    }
    // 其他逻辑
    return nil
    }
  5. 如果是调用其他库(标准库、企业公共库、开源第三方库等)获取到错误时,请使用 errors.Wrap 添加堆栈信息

    func f() error {
    err := json.Unmashal(&a, data)
    if err != nil {
    return errors.Wrap(err, "其他附加信息")
    } // 其他逻辑
    return nil
    }
    • 切记,不要每个地方都是用errors.Wrap只需要在错误第一次出现时进行 errors.Wrap 即可
    • 根据场景进行判断是否需要将其他库的原始错误隐藏掉,例如可以把 repository 层的数据库相关错误隐藏,返回业务错误码,避免后续我们分割微服务或者更换 ORM 库时需要去修改上层代码
    • 注意在基础库,被大量引入的第三方库编写时一般不使用 errors.Wrap 避免堆栈信息重复
  6. 禁止每个出错的地方都打日志,只需要在进程的最开始的地方使用 %+v 进行统一打印,例如 http/rpc 服务的中间件

  7. 错误判断使用errors.Is进行比较

    func f() error {
    err := A()
    if errors.Is(err, io.EOF){
    return nil
    } // 其他逻辑
    return nil
    }
  8. 错误类型判断使用errors.As进行赋值

    func f() error(){
    err := A()
    var errA errorA
    if erros.As(err, &errA){
    //...
    } // 其他逻辑
    return nil
    }
  9. 如何判定错误的信息是否足够,想一想当你的代码出现问题需要排查的时候你的错误信息是否可以帮助你快速的定位问题,例如我们在请求中一般会输出参数信息,用于辅助判断错误

  10. 对于业务错误,推荐在一个统一的地方创建一个错误字典,错误字典里面应该包含错误的 code,并且在日志中作为独立字段打印,方便做业务告警的判断,错误必须有清晰的错误文档

  11. 不需要返回,被忽略的错误必须输出日志信息

  12. 同一个地方不停的报错,最好不要不停输出错误日志,这样可能会导致被大量的错误日志信息淹没,无法排查问题,比较好的做法是打印一次错误详情,然后打印出错误出现的次数

  13. 对同一个类型的错误,采用相同的模式,例如参数错误,不要有的返回 404 有的返回 200

  14. 处理错误的时候,需要处理已分配的资源,使用 defer 进行清理,例如文件句柄

3. panic or error?

  1. 在 Go 中 panic 会导致程序直接退出,是一个致命的错误,如果使用 panic recover 进行处理的话,会存在很多问题

    • 性能问题,频繁 panic recover 性能不好
    • 容易导致程序异常退出,只要有一个地方没有处理到就会导致程序进程整个退出
    • 不可控,一旦 panic 就将处理逻辑移交给了外部,我们并不能预设外部包一定会进行处理
  2. 什么时候使用panic呢?
    • 对于真正意外的情况,那些表示不可恢复的程序错误, 例如索引越界不可恢复的环境问题栈溢出, 我们才能使用panic
  3. 使用 error 处理有哪些好处?
    • 简单
    • 考虑失败,而不是成功
    • 没有隐藏的控制流
    • 完全交给你来控制error

4.为什么标准库中 errors.New 会返回一个指针

翻看标准库的源代码我们可以发现, errors 库中的 errorString 结构体实现了 error 接口,为什么在 New 一个 error 的时候会返回一个结构体的指针呢?

// 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
}

先来看一个例子,我们同样创建了 errorString 的结构体,自定义的和标准库中的唯一不同就是,自建的这个返回的是值,而不是指针。

在 main 函数的对比中我们就可以发现,我们自定义的 errorString 在对比的时候只要对应的字符串相同就会返回 true,但是标准库的包不会。

这是因为,在对比两个 struct 是否相同的时候,会去对比,这两个 struct 里面的各个字段是否是相同的,如果相同就返回true,但是对比指针的时候会去判断两个指针的地址是否一致

如果字符串相等就返回 true 会导致什么问题呢?

如果我有两个包,定义了两个错误,他们其实是两个相同的字符串,在其他库调用对比的时候,可能会由于不同的书写顺序,走进不同的分支导致一些奇奇怪怪的错误

// 自定义错误类型

import "erros"

type errorString struct {
text string
} func (e errorString) Error() string {
return e.text
} // New 创建一个自定义错误,返回的时结构体,而不是指针
func New(s string) error {
return errorString{text: s}
} // 使用自定义错误实例化错误,返回的时 errorString结构体
var errorString1 = New("test a") // 使用标准库实例化一个错误,是一个指针
var err1 = errors.New("test b") func main() {
if errorString1 == New("test a") {
fmt.Println("err string a") // 会输出
} // 两次实例化的指针,地址并不一样
if err1 == errors.New("test b") {
fmt.Println("err b") // 不会输出
}
}

5.错误定义与判断

5.1 Sentinel Error

哨兵错误,就是定义一些包级别的错误变量,然后在调用的时候外部包可以直接对比变量进行判定,在标准库当中大量的使用了这种方式

例如io库中定义的错误

// EOF is the error returned by Read when no more input is available.
// Functions should return EOF only to signal a graceful end of input.
// If the EOF occurs unexpectedly in a structured data stream,
// the appropriate error is either ErrUnexpectedEOF or some other error
// giving more detail.
var EOF = errors.New("EOF") // ErrUnexpectedEOF means that EOF was encountered in the
// middle of reading a fixed-size block or data structure.
var ErrUnexpectedEOF = errors.New("unexpected EOF") // ErrNoProgress is returned by some clients of an io.Reader when
// many calls to Read have failed to return any data or error,
// usually the sign of a broken io.Reader implementation.
var ErrNoProgress = errors.New("multiple Read calls return no data or error")

在外部判定的时候一般使用等值判定或者使用 errors.Is 进行判断

if err == io.EOF {
//...
} if errors.Is(err, io.EOF){
//...
}

这种错误处理方式有一个问题是,将 error 当做包的 API 暴露给了第三方,这样会导致在做重构或者升级的时候很麻烦,并且这种方式包含的错误信息会十分的有限

5.2 error types

通过类型断言的方式判断错误类型

type MyStruct struct {
s string
name string
path string
} // 使用的时候
func f() {
// 类型断言
switch err.(type) { case *MyStruct:
// ...
case others:
// ...
}
}

这种方式相对于哨兵来说,可以包含更加丰富的信息,但是同样也将错误的类型暴露给了外部,例如标准库中的os.PathError

5.3 Opaque errors

不透明的错误处理,这种方式最大的特点就是只返回错误,暴露错误判定接口,不返回类型,这样可以减少 API 的暴露,后续的处理会比较灵活,这个一般用在公共库会比较好

type temporary interface {
Temporary() bool
} func IsTemporary(err error) bool {
te, ok := err.(temporary)
return ok && te.Temporary()
}

6. 错误处理优化

在 go 中常常会存在大量的 if err 代码,下面介绍两种常见的减少这种代码的方式

6.1 bufio.scan

对比下面两个函数的处理我们可以发现, count2 使用 sc.Scan 之后一个 if err 的判断都没有,极大的简化了代码,这是因为在 sc.Scan 做了很多处理,像很多类似的,需要循环读取的都可以考虑像这样包装之后进行处理,这样外部包调用的时候就会非常简洁

// 统计文件行数
func count(r io.Reader) (int, error) {
var (
br = bufio.NewReader(r)
lines int
err error
) for {
// 读取到换行符就说明是一行
_, err = br.ReadString('\n')
lines++
if err != nil {
break
}
} // 当错误是 EOF 的时候说明文件读取完毕了
if err != io.EOF {
return 0, err
} return lines, err
} // good
func count2(r io.Reader) (int, error) {
var (
sc = bufio.NewScanner(r)
lines int
) for sc.Scan() {
lines++
} return lines, sc.Err()
}

6.2 error writer

一般代码

_, err = fd.Write(p0[a:b])
if err != nil {
return err
}
_, err = fd.Write(p1[c:d])
if err != nil {
return err
}
_, err = fd.Write(p2[e:f])
if err != nil {
return err
}
// and so on

使用errWriter

type errWriter struct {
w io.Writer
err error
} func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
} // 使用时
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
return ew.err
}

这种用法,将重复的逻辑进行了封装,然后把 error 暂存,然后我们就只需要在最后判断一下 error 就行了

7.wrap error 错误装饰器

7.1 errors.wrap 有何作用?

errors.wrap有何作用,为什么不用标准库的 fmt.Errorf("%w")?

标准库的源码,我们可以发现当 p.wrappedErr != nil 的时候(也就是有 %w)的时候,会使用一个 wrapError 将错误包装,看 wrapError 的源码可以发现,这个方法只是包装了一下原始错误,并且可以做到附加一些文本信息,但是没有堆栈信息。

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
} type wrapError struct {
msg string
err error
} func (e *wrapError) Error() string {
return e.msg
} func (e *wrapError) Unwrap() error {
return e.err
}

7.2 为什么不允许处处使用 errors.Wrap?

因为每一次 errors.Wrap的调用都会为错误添加堆栈信息,如果处处调用那会有大量的无用堆栈,先看一下只有一处 wrap

func main() {
fmt.Printf("err: %+v", c())
} func a() error {
return errors.Wrap(fmt.Errorf("xxx"), "test")
} func b() error {
return a()
} func c() error {
return b()
}

输出如下:

err: xxx
test
main.a
/home/ll/project/Go-000/Week02/blog/wrap.go:14
main.b
/home/ll/project/Go-000/Week02/blog/wrap.go:18
main.c
/home/ll/project/Go-000/Week02/blog/wrap.go:22
main.main
/home/ll/project/Go-000/Week02/blog/wrap.go:10
runtime.main
/usr/local/go/src/runtime/proc.go:204
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1374

再看多处 wrap 的现象

func main() {
fmt.Printf("err: %+v", c())
} func a() error {
return errors.Wrap(fmt.Errorf("xxx"), "a")
} func b() error {
return errors.Wrap(a(), "b")
} func c() error {
return errors.Wrap(b(), "c")
}

可以看到每一处 wrap 都添加了一次堆栈信息

err: xxx
a
main.a
/home/ll/project/Go-000/Week02/blog/wrap.go:14
main.b
/home/ll/project/Go-000/Week02/blog/wrap.go:18
main.c
/home/ll/project/Go-000/Week02/blog/wrap.go:22
main.main
/home/ll/project/Go-000/Week02/blog/wrap.go:10
runtime.main
/usr/local/go/src/runtime/proc.go:204
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1374
b
main.b
/home/ll/project/Go-000/Week02/blog/wrap.go:18
main.c
/home/ll/project/Go-000/Week02/blog/wrap.go:22
main.main
/home/ll/project/Go-000/Week02/blog/wrap.go:10
runtime.main
/usr/local/go/src/runtime/proc.go:204
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1374
c
main.c
/home/ll/project/Go-000/Week02/blog/wrap.go:22
main.main
/home/ll/project/Go-000/Week02/blog/wrap.go:10
runtime.main
/usr/local/go/src/runtime/proc.go:204
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1374

8. 错误判断原理

8.1 errors.Is


func Is(err, target error) bool {
if target == nil {
return err == target
}
// 通过反射判读 target 是否可以被比较
isComparable := reflectlite.TypeOf(target).Comparable()
for {
// 循环判断是否相等
if isComparable && err == target {
return true
}
// 判断是否实现了 is 接口,如果有实现就直接判断
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
} // 去判断是否实现了 unwrap 的接口,如果实现了就进行 unwrap
if err = Unwrap(err); err == nil {
return false
}
}
}

8.2 errors.As

和 is 的逻辑类似,就是不断的进行 unwrap 进行比较,只要有一个相同就返回,如果一直到底都不行就返回 false

func As(err error, target interface{}) bool {
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
targetType := typ.Elem()
for err != nil {
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
err = Unwrap(err)
}
return false
}

Go错误处理正确姿势的更多相关文章

  1. Spring Boot 处理 REST API 错误的正确姿势

    摘要:如何正确的处理API的返回信息,让返回的错误信息提供更多的含义是一个非常值得做的功能.默认一般返回的都是难以理解的堆栈信息,然而这些信息也许对于API的客户端来说有可能并没有多大用途,并没有多大 ...

  2. Golang错误和异常处理的正确姿势

    Golang错误和异常处理的正确姿势 错误和异常是两个不同的概念,非常容易混淆.很多程序员习惯将一切非正常情况都看做错误,而不区分错误和异常,即使程序中可能有异常抛出,也将异常及时捕获并转换成错误.从 ...

  3. Redis实现分布式锁的正确姿势

    分布式锁一般有三种实现方式:1. 数据库乐观锁:2. 基于Redis的分布式锁:3. 基于ZooKeeper的分布式锁.本篇博客将介绍第二种方式,基于Redis实现分布式锁.虽然网上已经有各种介绍Re ...

  4. 【分布式缓存系列】Redis实现分布式锁的正确姿势

    一.前言 在我们日常工作中,除了Spring和Mybatis外,用到最多无外乎分布式缓存框架——Redis.但是很多工作很多年的朋友对Redis还处于一个最基础的使用和认识.所以我就像把自己对分布式缓 ...

  5. Redis全方位详解--数据类型使用场景和redis分布式锁的正确姿势

    一.Redis数据类型 1.string string是Redis的最基本数据类型,一个key对应一个value,每个value最大可存储512M.string一半用来存图片或者序列化的数据. 2.h ...

  6. 读取ClassPath下resource文件的正确姿势

    1.前言 为什么要写这篇文章?身为Java程序员你有没有过每次需要读取 ClassPath 下的资源文件的时候,都要去百度一下,然后看到下面的这种答案: Thread.currentThread(). ...

  7. laravel-nestedset:多级无限分类正确姿势

    laravel-nestedset:多级无限分类正确姿势   laravel-nestedset是一个关系型数据库遍历树的larvel4-5的插件包 目录: Nested Sets Model简介 安 ...

  8. Flutter Webview添加Cookie的正确姿势

    场景 h5页面要从cookie里面取数据,所以需要在flutter webview的cookie里面塞一些数据,设置的数据多达十几条:按照网上查的使用方式来设置,通过fiddler抓包发现,只能生效一 ...

  9. 判断是否为gif/png图片的正确姿势

    判断是否为gif/png图片的正确姿势 1.在能取到图片后缀的前提下 1 2 3 4 5 6 7 8 9 //假设这是一个网络获取的URL NSString *path = @"http:/ ...

随机推荐

  1. spring-1-spring介绍和IOC容器开发

    一.介绍 1.版本 2.下载(jar包依赖) 下载 所以搜索:https://repo.spring.io/release/org/springframework/spring/ 文件分配 maven ...

  2. ThinkPHP3.2.3使用PHPExcel类操作excel导入读取excel

    方法一: 1. 下载PHPExcel并保存在如下位置: 2. 在控制器中引用 vendor("PHPExcel.PHPExcel"); $objReader = \PHPExcel ...

  3. 基于小熊派Hi3861鸿蒙开发的IoT物联网学习【四】

    一.互斥锁基本概念: 1.互斥锁又称互斥型信号量,是一种特殊的二值性信号量[二值型信号量可以理解为任务与中断间或者两个任务间的标志,该标志非"满"即"空"],用 ...

  4. redis数据类型及应用场景

    0.key的通用操作 KEYS * keys a keys a* 查看已存在所有键的名字 ****TYPE 返回键所存储值的类型 ****EXPIRE\ PEXPIRE 以秒\毫秒设定生存时间 *** ...

  5. 基于pygame框架的打飞机小游戏

    import pygame from pygame.locals import * import time import random class Base(object): "" ...

  6. 又一本springmvc学习指南 之---第22篇 springmvc 加载.xml文件的bean标签的过程

    writedby 张艳涛,今天看spring mvc 学习指南的第2章,特意提下这个作者是how tomcat works 俩个作者之一, 喜欢上一本书的风格,使用案例来讲述原理, 在做第一个案例的时 ...

  7. zabbix latest.php SQL注入漏洞(CVE-2016-10134)

    Zabbix 2.2.14之前的版本和3.0.4之前的3.0版本 latest.php页面提取cookie中的zbx_sessionid的后16位 246c58ba963457ef http://19 ...

  8. Adobe ColdFusion 反序列化漏洞(CVE-2017-3066)

    影响版本 以下版本受到影响:Adobe ColdFusion (2016 release) Update 3及之前的版本,ColdFusion 11 Update 11及之前的版本,ColdFusio ...

  9. 记一次针对excel导出的优化

    最近发现我们系统导出excel文件时由于是导出百万级数据导出,速度过慢并且内存占用多,故进行了下面的一次优化. 我们使用apache的poi进行excel文件操作 主要耗时: 1.从数据库得到需要导出 ...

  10. XMAPP搭建DVWA靶机

    1  环境搭建 XMAPP+DVWA (我在win10下搭的环境) 更改了xmapp中Apache的两个端口号: dvwa/config中密钥和端口号按自己情况填好: dvwa/config中文件改为 ...